233 lines
5.1 KiB
Go
233 lines
5.1 KiB
Go
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
|
|
}
|