From 57d1f319f51c6cc6d8c93d3e942a1126b475e1bd Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 15 Oct 2024 08:40:11 +0200 Subject: [PATCH] feat: support confirm (boolean) values Use `optBool` to convert any value to a boolean to be used in the template. A new `type` field can be set for an option. Possible values are: - input: text field - select: single select field - confirm: ask for a yes/no confirmation If no type is set, if some values are set type will be select, else it will be an input. This is just for backward compatibility, type should be set. Signed-off-by: Yves Brissaud --- README.md | 3 +++ docs/index.markdown | 3 +++ internal/prompt/prompt.go | 41 ++++++++++++++++++++++++++++----------- runkit/read.go | 11 +++++++++++ runkit/run.go | 9 +++++++++ runkit/types.go | 12 ++++++++++-- 6 files changed, 66 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 80056b1..1fc5588 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ actions: # A list of options that can be provided by the user. opts: - name: OPTION_NAME # Name of the option. Also used in the local override or with `--opt` flag. + type: input|select|confirm # Type of the option. desc: DESCRIPTION # Description, rendered in the documentation of the action. prompt: PROMPT # A specific prompt to ask the user for the value. no-prompt: true|false # If set to true, the option will not be prompted to the user. @@ -155,6 +156,8 @@ actions: # The environment variable needs to be defined in the `env` section. # - `{{opt "OPTION"}}` will be replaced by the value of the option `OPTION`. # The value needs to be provided by the local configuration, on the command line or interactively. + # - `{{optBool "OPTION"}}` is equivalent to `{{opt "OPTION"}}` but will return the value as a boolean. + # True values are `1`, `t`, `T`, `TRUE`, `true` and `True`. Everything else is considered as false. # - `{{sh "COMMAND"}}` will be replaced by the output of the shell command `COMMAND`. # The command will be run using https://github.com/mvdan/sh without a standard input. cmd: COMMAND diff --git a/docs/index.markdown b/docs/index.markdown index 7eddcc5..e61c0fb 100644 --- a/docs/index.markdown +++ b/docs/index.markdown @@ -130,6 +130,7 @@ actions: # A list of options that can be provided by the user. opts: - name: OPTION_NAME # Name of the option. Also used in the local override or with `--opt` flag. + type: input|select|confirm # Type of the option. desc: DESCRIPTION # Description, rendered in the documentation of the action. prompt: PROMPT # A specific prompt to ask the user for the value. no-prompt: true|false # If set to true, the option will not be prompted to the user. @@ -148,6 +149,8 @@ actions: # The environment variable needs to be defined in the `env` section. # - `{{opt "OPTION"}}` will be replaced by the value of the option `OPTION`. # The value needs to be provided by the local configuration, on the command line or interactively. + # - `{{optBool "OPTION"}}` is equivalent to `{{opt "OPTION"}}` but will return the value as a boolean. + # True values are `1`, `t`, `T`, `TRUE`, `true` and `True`. Everything else is considered as false. # - `{{sh "COMMAND"}}` will be replaced by the output of the shell command `COMMAND`. # The command will be run using https://github.com/mvdan/sh without a standard input. cmd: COMMAND diff --git a/internal/prompt/prompt.go b/internal/prompt/prompt.go index 9664702..f06b781 100644 --- a/internal/prompt/prompt.go +++ b/internal/prompt/prompt.go @@ -3,6 +3,7 @@ package prompt import ( "cmp" "errors" + "strconv" "strings" "github.com/charmbracelet/huh" @@ -53,10 +54,11 @@ func Ask(action *runkit.Action, opts map[string]string) (map[string]string, erro } var ( - err error - form *huh.Form - fields []huh.Field - asked []string + err error + form *huh.Form + fields []huh.Field + asked []string + boolAsked []string ) for _, opt := range action.Options { @@ -67,27 +69,41 @@ func Ask(action *runkit.Action, opts map[string]string) (map[string]string, erro continue } opt := opt - if len(opt.Values) == 0 { + + var ( + title = cmp.Or(opt.Prompt, cmp.Or(opt.Description, opt.Name)) + description = sugar.If(title != opt.Description, opt.Description, "") + ) + switch opt.Type { + case runkit.OptTypeInput: fields = append(fields, huh.NewInput(). - Title(cmp.Or(opt.Prompt, cmp.Or(opt.Description, opt.Name))). + Title(title). Key(opt.Name). - Description(opt.Description). + Description(description). Placeholder(opt.Default). Suggestions(sugar.If(opt.Default != "", []string{opt.Default}, nil)). Validate(checkRequired(opt.Required))) - } else { + asked = append(asked, opt.Name) + case runkit.OptTypeSelect: fields = append(fields, huh.NewSelect[string](). - Title(cmp.Or(opt.Prompt, cmp.Or(opt.Description, opt.Name))). + Title(title). Key(opt.Name). - Description(opt.Description). + Description(description). Validate(checkRequired(opt.Required)). Options(pizza.Map(opt.Values, func(str string) huh.Option[string] { return huh.NewOption(str, str).Selected(str == opt.Default) })...)) + asked = append(asked, opt.Name) + case runkit.OptTypeConfirm: + fields = append(fields, + huh.NewConfirm(). + Title(title). + Key(opt.Name). + Description(description)) + boolAsked = append(boolAsked, opt.Name) } - asked = append(asked, opt.Name) } if len(fields) == 0 { @@ -102,6 +118,9 @@ func Ask(action *runkit.Action, opts map[string]string) (map[string]string, erro for _, optName := range asked { opts[optName] = form.GetString(optName) } + for _, optName := range boolAsked { + opts[optName] = strconv.FormatBool(form.GetBool(optName)) + } return opts, nil } diff --git a/runkit/read.go b/runkit/read.go index 99fb413..6ee976b 100644 --- a/runkit/read.go +++ b/runkit/read.go @@ -188,6 +188,17 @@ func decodeConfig(rk *RunKit, src string, runxConfig []byte) error { a.isDefault = true } + for i, o := range a.Options { + if o.Type == OptTypeNotSet { + if len(o.Values) > 0 { + o.Type = OptTypeSelect + } else { + o.Type = OptTypeInput + } + a.Options[i] = o + } + } + actions = append(actions, a) } config.Actions = actions diff --git a/runkit/run.go b/runkit/run.go index 315632f..4118946 100644 --- a/runkit/run.go +++ b/runkit/run.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "strconv" "strings" "text/template" @@ -118,6 +119,14 @@ func (r *Runnable) compute() error { "opt": func(optName string) string { return r.data.Opts[optName] }, + "optBool": func(optName string) bool { + o, ok := r.data.Opts[optName] + if !ok { + return false + } + v, _ := strconv.ParseBool(o) + return v + }, "sh": func(cmdName string) (string, error) { v, ok := shells[cmdName] if !ok { diff --git a/runkit/types.go b/runkit/types.go index f47b08a..03eef84 100644 --- a/runkit/types.go +++ b/runkit/types.go @@ -29,6 +29,7 @@ type ( Opt struct { Name string `yaml:"name" json:"name"` + Type OptType `yaml:"type,omitempty" json:"type,omitempty"` Description string `yaml:"desc" json:"desc,omitempty"` NoPrompt bool `yaml:"no-prompt,omitempty" json:"no-prompt,omitempty"` Prompt string `yaml:"prompt,omitempty" json:"prompt,omitempty"` @@ -39,6 +40,8 @@ type ( ActionType string + OptType string + LocalConfig struct { Ref string `yaml:"ref,omitempty" json:"ref,omitempty"` Images map[string]ConfigImage `yaml:"images,omitempty" json:"images,omitempty"` @@ -58,8 +61,13 @@ type ( const ( ActionTypeRun ActionType = "run" ActionTypeBuild ActionType = "build" + + OptTypeNotSet OptType = "" + OptTypeInput OptType = "input" + OptTypeSelect OptType = "select" + OptTypeConfirm OptType = "confirm" ) -func (a *Action) IsDefault() bool { - return a.isDefault +func (action *Action) IsDefault() bool { + return action.isDefault }