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 }