package generator import ( "bufio" "errors" "fmt" "io" "regexp" "strconv" "github.com/fatih/color" "github.com/nicholas-fedor/shoutrrr/pkg/format" ) // errInvalidFormat indicates an invalid user input format. var ( errInvalidFormat = errors.New("invalid format") errRequired = errors.New("field is required") errNotANumber = errors.New("not a number") errInvalidBoolFormat = errors.New("answer must be yes or no") ) // ValidateFormat wraps a boolean validator to return an error on false results. func ValidateFormat(validator func(string) bool) func(string) error { return func(answer string) error { if validator(answer) { return nil } return errInvalidFormat } } // Required validates that the input contains at least one character. func Required(answer string) error { if answer == "" { return errRequired } return nil } // UserDialog facilitates question/answer-based user interaction. type UserDialog struct { reader io.Reader writer io.Writer scanner *bufio.Scanner props map[string]string } // NewUserDialog initializes a UserDialog with safe defaults. func NewUserDialog(reader io.Reader, writer io.Writer, props map[string]string) *UserDialog { if props == nil { props = map[string]string{} } return &UserDialog{ reader: reader, writer: writer, scanner: bufio.NewScanner(reader), props: props, } } // Write sends a message to the user. func (ud *UserDialog) Write(message string, v ...any) { if _, err := fmt.Fprintf(ud.writer, message, v...); err != nil { _, _ = fmt.Fprint(ud.writer, "failed to write to output: ", err, "\n") } } // Writelnf writes a formatted message to the user, completing a line. func (ud *UserDialog) Writelnf(format string, v ...any) { ud.Write(format+"\n", v...) } // Query prompts the user and returns regex groups if the input matches the validator pattern. func (ud *UserDialog) Query(prompt string, validator *regexp.Regexp, key string) []string { var groups []string ud.QueryString(prompt, ValidateFormat(func(answer string) bool { groups = validator.FindStringSubmatch(answer) return groups != nil }), key) return groups } // QueryAll prompts the user and returns multiple regex matches up to maxMatches. func (ud *UserDialog) QueryAll( prompt string, validator *regexp.Regexp, key string, maxMatches int, ) [][]string { var matches [][]string ud.QueryString(prompt, ValidateFormat(func(answer string) bool { matches = validator.FindAllStringSubmatch(answer, maxMatches) return matches != nil }), key) return matches } // QueryString prompts the user and returns the answer if it passes the validator. func (ud *UserDialog) QueryString(prompt string, validator func(string) error, key string) string { if validator == nil { validator = func(string) error { return nil } } answer, foundProp := ud.props[key] if foundProp { err := validator(answer) colAnswer := format.ColorizeValue(answer, false) colKey := format.ColorizeProp(key) if err == nil { ud.Writelnf("Using prop value %v for %v", colAnswer, colKey) return answer } ud.Writelnf("Supplied prop value %v is not valid for %v: %v", colAnswer, colKey, err) } for { ud.Write("%v ", prompt) color.Set(color.FgHiWhite) if !ud.scanner.Scan() { if err := ud.scanner.Err(); err != nil { ud.Writelnf(err.Error()) continue } // Input closed, return an empty string return "" } answer = ud.scanner.Text() color.Unset() if err := validator(answer); err != nil { ud.Writelnf("%v", err) ud.Writelnf("") continue } return answer } } // QueryStringPattern prompts the user and returns the answer if it matches the regex pattern. func (ud *UserDialog) QueryStringPattern( prompt string, validator *regexp.Regexp, key string, ) string { if validator == nil { panic("validator cannot be nil") } return ud.QueryString(prompt, func(s string) error { if validator.MatchString(s) { return nil } return errInvalidFormat }, key) } // QueryInt prompts the user and returns the answer as an integer if parseable. func (ud *UserDialog) QueryInt(prompt string, key string, bitSize int) int64 { validator := regexp.MustCompile(`^((0x|#)([0-9a-fA-F]+))|(-?[0-9]+)$`) var value int64 ud.QueryString(prompt, func(answer string) error { groups := validator.FindStringSubmatch(answer) if len(groups) < 1 { return errNotANumber } number := groups[0] base := 0 if groups[2] == "#" { // Explicitly treat #ffa080 as hexadecimal base = 16 number = groups[3] } var err error value, err = strconv.ParseInt(number, base, bitSize) if err != nil { return fmt.Errorf("parsing integer from %q: %w", answer, err) } return nil }, key) return value } // QueryBool prompts the user and returns the answer as a boolean if parseable. func (ud *UserDialog) QueryBool(prompt string, key string) bool { var value bool ud.QueryString(prompt, func(answer string) error { parsed, ok := format.ParseBool(answer, false) if ok { value = parsed return nil } return fmt.Errorf( "%w: use %v or %v", errInvalidBoolFormat, format.ColorizeTrue("yes"), format.ColorizeFalse("no"), ) }, key) return value }