Skip to content

Commit

Permalink
Added missing AskOpts (AlecAivazis#216)
Browse files Browse the repository at this point in the history
* updated readme

* added ask opt to set help input rune

* removed global icon definitions

* templates refer to icon field

* added explicit icon sets to tests

* added missing icon set in test runner

* removed errant log

* added missing icon set in select test

* added WithIconSet AskOpt

* can set error icon

* Prompt interface now uses PromptConfig

* tests use default icon set instead of hardcoded value

* config comes first in prompt inteface

* ran gofmt

* renamed WithHelpInputRune to WithHelpInput

* removed HelpInput from icon set

* removed unnecessary type cast

* removed globally defined default icon set

* added AskOpt to set default filter

* documented WithFilter

* renamed WithIconSet to WithIcons
  • Loading branch information
AlecAivazis authored Jun 3, 2019
1 parent 5317b92 commit 10099b4
Show file tree
Hide file tree
Showing 23 changed files with 528 additions and 362 deletions.
78 changes: 55 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,29 +208,45 @@ the result. If neither of those are present, notepad (on Windows) or vim (Linux

## Filtering options in Select and MultiSelect

The user can filter for options by typing while the prompt is active. This will filter out all options that don't contain the
typed string anywhere in their name, ignoring case. This default filtering behavior is provided by the `DefaultFilter` function.
By defaualt, the user can filter for options by typing while the prompt is active. This will filter out all options that don't contain the
typed string anywhere in their name, ignoring case.

A custom filter function can also be provided to change this default behavior by providing a value for the `Filter` field:

```golang
&Select{
Message: "Choose a color:",
Options: []string{"red", "blue", "green"},
Filter: func(filter string, options []string) (filtered []string) {
result := DefaultFilter(filter, options)
Filter: func(filter string, options []string) ([]string) {
filtered := []string{}

for _, v := range result {
if len(v) >= 5 {
filtered = append(filtered, v)
}
}
return
return filtered
},
}
```

While the example above is contrived, this allows for use cases where "smarter" filtering might be useful, for example, when
options are backed by more complex types and filtering might need to occur on more metadata than just the displayed name.
You can also change the default filter applied with the `survey.WithFilter` `AskOpt`:

```golang
func myFilter(filter string, options []string) ([]string) {
filtered := []string{}
for _, v := range result {
if len(v) >= 5 {
filtered = append(filtered, v)
}
}

return filtered
}


survey.Ask(prompt, &color, survey.WithFilter(myFilter))
```

## Validation

Expand Down Expand Up @@ -289,12 +305,11 @@ All of the prompts have a `Help` field which can be defined to provide more info
### Changing the input rune

In some situations, `?` is a perfectly valid response. To handle this, you can change the rune that survey
looks for by setting the `HelpInputRune` variable in `survey/core`:
looks for by passing an `AskOpt` to `Ask` or `AskOne`:

```golang
import (
"github.com/AlecAivazis/survey/v2"
surveyCore "github.com/AlecAivazis/survey/v2/core"
)

number := ""
Expand All @@ -303,9 +318,7 @@ prompt := &survey.Input{
Help: "I couldn't come up with one.",
}

surveyCore.HelpInputRune = '^'

survey.AskOne(prompt, &number)
survey.AskOne(prompt, &number, survey.WithHelpInput('^'))
```

## Custom Types
Expand Down Expand Up @@ -340,17 +353,36 @@ survey.AskOne(

## Customizing Output

Customizing the icons and various parts of survey can easily be done by setting the following variables
in `survey/core`:

| name | default | description |
| ------------------ | ------- | ------------------------------------------------------------- |
| ErrorIcon | X | Before an error |
| HelpIcon | i | Before help text |
| QuestionIcon | ? | Before the message of a prompt |
| SelectFocusIcon | > | Marks the current focus in `Select` and `MultiSelect` prompts |
| UnmarkedOptionIcon | [ ] | Marks an unselected option in a `MultiSelect` prompt |
| MarkedOptionIcon | [x] | Marks a chosen selection in a `MultiSelect` prompt |
Customizing the icons and various parts of survey can easily be done by passing the `WithIcons` option
to `Ask` or `AskOne`:

```golang
import (
"github.com/AlecAivazis/survey/v2"
)

number := ""
prompt := &survey.Input{
Message: "If you have this need, please give me a reasonable message.",
Help: "I couldn't come up with one.",
}

survey.AskOne(prompt, &number, survey.WithIcons(function(icons *survey.IconSet) {
// you can set any icons
icons.Question = ""
}))
```

The icons available for updating are:

| name | default | description |
| -------------- | ------- | ------------------------------------------------------------- |
| Error | X | Before an error |
| Help | i | Before help text |
| Question | ? | Before the message of a prompt |
| SelectFocus | > | Marks the current focus in `Select` and `MultiSelect` prompts |
| UnmarkedOption | [ ] | Marks an unselected option in a `MultiSelect` prompt |
| MarkedOption | [x] | Marks a chosen selection in a `MultiSelect` prompt |

## Testing

Expand Down
45 changes: 30 additions & 15 deletions confirm.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ package survey
import (
"fmt"
"regexp"

"github.com/AlecAivazis/survey/v2/core"
)

// Confirm is a regular text input that accept yes/no answers. Response type is a bool.
type Confirm struct {
core.Renderer
Renderer
Message string
Default bool
Help string
Expand All @@ -20,17 +18,18 @@ type ConfirmTemplateData struct {
Confirm
Answer string
ShowHelp bool
Config *PromptConfig
}

// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
var ConfirmQuestionTemplate = `
{{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
{{- color "green+hb"}}{{ QuestionIcon }} {{color "reset"}}
{{- if .ShowHelp }}{{- color "cyan"}}{{ .Config.Icons.Help }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
{{- color "green+hb"}}{{ .Config.Icons.Question }} {{color "reset"}}
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
{{- if .Answer}}
{{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}}
{{- else }}
{{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ HelpInputRune }} for help]{{color "reset"}} {{end}}
{{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}}
{{- color "white"}}{{if .Default}}(Y/n) {{else}}(y/N) {{end}}{{color "reset"}}
{{- end}}`

Expand All @@ -47,7 +46,7 @@ func yesNo(t bool) string {
return "No"
}

func (c *Confirm) getBool(showHelp bool) (bool, error) {
func (c *Confirm) getBool(showHelp bool, config *PromptConfig) (bool, error) {
cursor := c.NewCursor()
rr := c.NewRuneReader()
rr.SetTermMode()
Expand All @@ -72,10 +71,14 @@ func (c *Confirm) getBool(showHelp bool) (bool, error) {
answer = false
case val == "":
answer = c.Default
case val == string(core.HelpInputRune) && c.Help != "":
case val == config.HelpInput && c.Help != "":
err := c.Render(
ConfirmQuestionTemplate,
ConfirmTemplateData{Confirm: *c, ShowHelp: true},
ConfirmTemplateData{
Confirm: *c,
ShowHelp: true,
Config: config,
},
)
if err != nil {
// use the default value and bubble up
Expand All @@ -85,12 +88,16 @@ func (c *Confirm) getBool(showHelp bool) (bool, error) {
continue
default:
// we didnt get a valid answer, so print error and prompt again
if err := c.Error(fmt.Errorf("%q is not a valid answer, please try again.", val)); err != nil {
if err := c.Error(config, fmt.Errorf("%q is not a valid answer, please try again.", val)); err != nil {
return c.Default, err
}
err := c.Render(
ConfirmQuestionTemplate,
ConfirmTemplateData{Confirm: *c, ShowHelp: showHelp},
ConfirmTemplateData{
Confirm: *c,
ShowHelp: showHelp,
Config: config,
},
)
if err != nil {
// use the default value and bubble up
Expand All @@ -116,23 +123,31 @@ func (c *Confirm) Prompt(config *PromptConfig) (interface{}, error) {
// render the question template
err := c.Render(
ConfirmQuestionTemplate,
ConfirmTemplateData{Confirm: *c},
ConfirmTemplateData{
Confirm: *c,
Config: config,
},
)
if err != nil {
return "", err
}

// get input and return
return c.getBool(false)
return c.getBool(false, config)
}

// Cleanup overwrite the line with the finalized formatted version
func (c *Confirm) Cleanup(val interface{}) error {
func (c *Confirm) Cleanup(config *PromptConfig, val interface{}) error {
// if the value was previously true
ans := yesNo(val.(bool))

// render the template
return c.Render(
ConfirmQuestionTemplate,
ConfirmTemplateData{Confirm: *c, Answer: ans},
ConfirmTemplateData{
Confirm: *c,
Answer: ans,
Config: config,
},
)
}
16 changes: 10 additions & 6 deletions confirm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,31 +30,31 @@ func TestConfirmRender(t *testing.T) {
"Test Confirm question output with default true",
Confirm{Message: "Is pizza your favorite food?", Default: true},
ConfirmTemplateData{},
fmt.Sprintf("%s Is pizza your favorite food? (Y/n) ", core.QuestionIcon),
fmt.Sprintf("%s Is pizza your favorite food? (Y/n) ", defaultAskOptions().PromptConfig.Icons.Question),
},
{
"Test Confirm question output with default false",
Confirm{Message: "Is pizza your favorite food?", Default: false},
ConfirmTemplateData{},
fmt.Sprintf("%s Is pizza your favorite food? (y/N) ", core.QuestionIcon),
fmt.Sprintf("%s Is pizza your favorite food? (y/N) ", defaultAskOptions().PromptConfig.Icons.Question),
},
{
"Test Confirm answer output",
Confirm{Message: "Is pizza your favorite food?"},
ConfirmTemplateData{Answer: "Yes"},
fmt.Sprintf("%s Is pizza your favorite food? Yes\n", core.QuestionIcon),
fmt.Sprintf("%s Is pizza your favorite food? Yes\n", defaultAskOptions().PromptConfig.Icons.Question),
},
{
"Test Confirm with help but help message is hidden",
Confirm{Message: "Is pizza your favorite food?", Help: "This is helpful"},
ConfirmTemplateData{},
fmt.Sprintf("%s Is pizza your favorite food? [%s for help] (y/N) ", core.QuestionIcon, string(core.HelpInputRune)),
fmt.Sprintf("%s Is pizza your favorite food? [%s for help] (y/N) ", defaultAskOptions().PromptConfig.Icons.Question, string(defaultAskOptions().PromptConfig.HelpInput)),
},
{
"Test Confirm help output with help message shown",
Confirm{Message: "Is pizza your favorite food?", Help: "This is helpful"},
ConfirmTemplateData{ShowHelp: true},
fmt.Sprintf("%s This is helpful\n%s Is pizza your favorite food? (y/N) ", core.HelpIcon, core.QuestionIcon),
fmt.Sprintf("%s This is helpful\n%s Is pizza your favorite food? (y/N) ", defaultAskOptions().PromptConfig.Icons.Help, defaultAskOptions().PromptConfig.Icons.Question),
},
}

Expand All @@ -64,6 +64,10 @@ func TestConfirmRender(t *testing.T) {

test.prompt.WithStdio(terminal.Stdio{Out: w})
test.data.Confirm = test.prompt

// set the runtime config
test.data.Config = &defaultAskOptions().PromptConfig

err = test.prompt.Render(
ConfirmQuestionTemplate,
test.data,
Expand Down Expand Up @@ -128,7 +132,7 @@ func TestConfirmPrompt(t *testing.T) {
c.ExpectString(
fmt.Sprintf(
"Is pizza your favorite food? [%s for help] (y/N)",
string(core.HelpInputRune),
string(defaultAskOptions().PromptConfig.HelpInput),
),
)
c.SendLine("?")
Expand Down
71 changes: 14 additions & 57 deletions core/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,9 @@ import (
"github.com/mgutz/ansi"
)

// DisableColor can be used to make testing reliable
var DisableColor = false

var (
// HelpInputRune is the rune which the user should enter to trigger
// more detailed question help
HelpInputRune = '?'

// ErrorIcon will be be shown before an error
ErrorIcon = "X"

// HelpIcon will be shown before more detailed question help
HelpIcon = "?"
// QuestionIcon will be shown before a question Message
QuestionIcon = "?"

// MarkedOptionIcon will be prepended before a selected multiselect option
MarkedOptionIcon = "[x]"
// UnmarkedOptionIcon will be prepended before an unselected multiselect option
UnmarkedOptionIcon = "[ ]"

// SelectFocusIcon is prepended to an option to signify the user is
// currently focusing that option
SelectFocusIcon = ">"
)

var TemplateFuncs = map[string]interface{}{
// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
"color": func(color string) string {
Expand All @@ -41,27 +19,19 @@ var TemplateFuncs = map[string]interface{}{
}
return ansi.ColorCode(color)
},
"HelpInputRune": func() string {
return string(HelpInputRune)
},
"ErrorIcon": func() string {
return ErrorIcon
},
"HelpIcon": func() string {
return HelpIcon
},
"QuestionIcon": func() string {
return QuestionIcon
},
"MarkedOptionIcon": func() string {
return MarkedOptionIcon
},
"UnmarkedOptionIcon": func() string {
return UnmarkedOptionIcon
},
"SelectFocusIcon": func() string {
return SelectFocusIcon
},
}

func RunTemplate(tmpl string, data interface{}) (string, error) {
t, err := getTemplate(tmpl)
if err != nil {
return "", err
}
buf := bytes.NewBufferString("")
err = t.Execute(buf, data)
if err != nil {
return "", err
}
return buf.String(), err
}

var (
Expand All @@ -88,16 +58,3 @@ func getTemplate(tmpl string) (*template.Template, error) {
memoMutex.Unlock()
return t, nil
}

func RunTemplate(tmpl string, data interface{}) (string, error) {
t, err := getTemplate(tmpl)
if err != nil {
return "", err
}
buf := bytes.NewBufferString("")
err = t.Execute(buf, data)
if err != nil {
return "", err
}
return buf.String(), err
}
Loading

0 comments on commit 10099b4

Please sign in to comment.