wer
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
.imdone/
|
||||
*.out
|
||||
vendor/**
|
||||
!vendor/vendor.json
|
||||
@@ -0,0 +1,9 @@
|
||||
language: go
|
||||
go:
|
||||
- tip
|
||||
before_install:
|
||||
- go get -t -v ./...
|
||||
script:
|
||||
- go test -race -coverprofile=coverage.txt -covermode=atomic
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Will Dixon
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,167 @@
|
||||
# WMenu[](https://travis-ci.org/dixonwille/wmenu) [](https://codecov.io/gh/dixonwille/wmenu)
|
||||
|
||||
Package wmenu creates menus for cli programs. It uses wlog for it's interface
|
||||
with the command line. It uses os.Stdin, os.Stdout, and os.Stderr with
|
||||
concurrency by default. wmenu allows you to change the color of the different
|
||||
parts of the menu. This package also creates it's own error structure so you can
|
||||
type assert if you need to. wmenu will validate all responses before calling any function. It will also figure out which function should be called so you don't have to.
|
||||
|
||||
[](https://asciinema.org/a/4lv3ash3ubtnsclindvzdf320)
|
||||
|
||||
## Import
|
||||
|
||||
I try and keep up with my tags. To use the version and stable it is recommended to use `govendor` or another vendoring tool that allows you to build your project for specific tags.
|
||||
|
||||
```sh
|
||||
govendor fetch github.com/dixonwille/wmenu@v4
|
||||
```
|
||||
|
||||
The above will grab the latest v4 at that time and mark it. It will then be stable for you to use.
|
||||
|
||||
I will try to support as many versions as possable but please be patient.
|
||||
|
||||
### V1.0.0 - Major Release [](https://goreportcard.com/report/gopkg.in/dixonwille/wmenu.v1) [](https://godoc.org/gopkg.in/dixonwille/wmenu.v1)
|
||||
|
||||
### V2.0.0 - Allowing an interface to be passed in for options [](https://goreportcard.com/report/gopkg.in/dixonwille/wmenu.v2) [](https://godoc.org/gopkg.in/dixonwille/wmenu.v2)
|
||||
|
||||
### V3.0.0 - Pass in the option to that option's function [](https://goreportcard.com/report/gopkg.in/dixonwille/wmenu.v3) [](https://godoc.org/gopkg.in/dixonwille/wmenu.v3)
|
||||
|
||||
### V4.0.0 - Now have an Action that supports multiple options [](https://goreportcard.com/report/gopkg.in/dixonwille/wmenu.v4) [](https://godoc.org/gopkg.in/dixonwille/wmenu.v4)
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* Force single selection
|
||||
* Allow multiple selection
|
||||
* Change the delimiter
|
||||
* Change the color of different parts of the menu
|
||||
* Easily see which option(s) are default
|
||||
* Change the symbol used for default option(s)
|
||||
* Ask simple yes and no questions
|
||||
* Validate all responses before calling any functions
|
||||
* With yes and no can accept:
|
||||
* yes, Yes, YES, y, Y
|
||||
* no, No, NO, n, N
|
||||
* Figure out which Action should be called (Options, Default, or Multiple Action)
|
||||
* Re-ask question if invalid response up to a certain number of times
|
||||
* Can change max number of times to ask before failing output
|
||||
* Change reader and writer
|
||||
* Clear the screen whenever the menu is brought up
|
||||
* Has its own error structure so you can type assert menu errors
|
||||
|
||||
### V2 - Adds these Features
|
||||
|
||||
* Allowing any interface to be passed through for the options.
|
||||
|
||||
### V3 - Adds these Features
|
||||
|
||||
* Pass the option chosen to that options function
|
||||
|
||||
### V4 - Adds these Features
|
||||
|
||||
* Have one function for both single and multiple select. Allowing the user to an easier way of handeling the request.
|
||||
|
||||
## Usage
|
||||
This is a simple use of the package. (**NOTE: THIS IS A V4 SAMPLE**)
|
||||
|
||||
``` go
|
||||
menu := wmenu.NewMenu("What is your favorite food?")
|
||||
menu.Action(func (opts []wmenu.Opt) error {fmt.Printf(opts[0].Text + " is your favorite food."); return nil})
|
||||
menu.Option("Pizza", nil, true, nil)
|
||||
menu.Option("Ice Cream", nil, false, nil)
|
||||
menu.Option("Tacos", nil, false, func(opt wmenu.Opt) error {
|
||||
fmt.Printf("Tacos are great")
|
||||
})
|
||||
err := menu.Run()
|
||||
if err != nil{
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
The output would look like this:
|
||||
|
||||
```
|
||||
0) *Pizza
|
||||
1) Ice Cream
|
||||
2) Tacos
|
||||
What is your favorite food?
|
||||
```
|
||||
|
||||
If the user just presses `[Enter]` then the option(s) with the `*` will be selected. This indicates that it is a default function. If they choose `1` then they would see `Ice Cream is your favorite food.`. This used the Action's function because the option selected didn't have a function along with it. But if they choose `2` they would see `Tacos are great`. That option did have a function with it which take precedence over Action.
|
||||
|
||||
You can you also use:
|
||||
|
||||
``` go
|
||||
menu.AllowMultiple()
|
||||
```
|
||||
|
||||
This will allow the user to select multiple options. The default delimiter is a `[space]`, but can be changed by using:
|
||||
|
||||
``` go
|
||||
menu.SetSeperator("some string")
|
||||
```
|
||||
|
||||
Another feature is the ability to ask yes or no questions.
|
||||
|
||||
``` go
|
||||
menu.IsYesNo(0)
|
||||
```
|
||||
|
||||
This will remove any options previously added options and hide the ones used for the menu. It will simply just ask yes or no. Menu will parse and validate the response for you. This option will always call the Action's function and pass in the option that was selected.
|
||||
|
||||
## V3+ - Release
|
||||
Allows the user to pass anything for the value so it can be retrieved later in the function. The following is to show case the power of this.
|
||||
|
||||
> The following was written in V3 but the concept holds for V4. V4 just changed `actFunc` to be `func([]wmenu.Opt) error` instead.
|
||||
|
||||
```go
|
||||
type NameEntity struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
}
|
||||
|
||||
optFunc := func(opt wmenu.Opt) error {
|
||||
fmt.Println("Option 0 was chosen.")
|
||||
return nil
|
||||
}
|
||||
actFunc := func(opt wmenu.Opt) error {
|
||||
name, ok := opt.Value.(NameEntity)
|
||||
if !ok {
|
||||
log.Fatal("Could not cast option's value to NameEntity")
|
||||
}
|
||||
fmt.Printf("%s has an id of %d.\n", opt.Text, opt.ID)
|
||||
fmt.Printf("Hello, %s %s.\n", name.FirstName, name.LastName)
|
||||
return nil
|
||||
}
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, os.Stdout, os.Stderr)
|
||||
menu.Action(actFunc)
|
||||
menu.Option("Option 0", NameEntity{"Bill", "Bob"}, true, optFunc)
|
||||
menu.Option("Option 1", NameEntity{"John", "Doe"}, false, nil)
|
||||
menu.Option("Option 2", NameEntity{"Jane", "Doe"}, false, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
The immediate output would be:
|
||||
|
||||
```
|
||||
Output:
|
||||
0) *Option 0
|
||||
1) Option 1
|
||||
2) Option 2
|
||||
Choose an option.
|
||||
```
|
||||
|
||||
Now if the user pushes `[ENTER]` the output would be `Options 0 was chosen.`. But now if either option 1 or 2 were chosen it would cast the options value to a NameEntity allowing the function to be able to gather both the first name and last name of the NameEntity. If you want though you can just pass in `nil` as the value or even a string (`"hello"`) since both of these implement the empty interface required by value. Just make sure to cast the values so you can use them appropriately.
|
||||
|
||||
## Further Reading
|
||||
This whole package has been documented and has a few examples in:
|
||||
* [godocs V1](https://godoc.org/gopkg.in/dixonwille/wmenu.v1)
|
||||
* [godocs V2](https://godoc.org/gopkg.in/dixonwille/wmenu.v2)
|
||||
* [godocs V3](https://godoc.org/gopkg.in/dixonwille/wmenu.v3)
|
||||
* [godocs V4](https://godoc.org/gopkg.in/dixonwille/wmenu.v4)
|
||||
|
||||
You should read the docs to find all functions and structures at your finger tips.
|
||||
@@ -0,0 +1,36 @@
|
||||
package wmenu
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var clear map[string]func()
|
||||
|
||||
func init() {
|
||||
clear = make(map[string]func())
|
||||
clear["linux"] = func() {
|
||||
cmd := exec.Command("clear")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Run()
|
||||
}
|
||||
clear["darwin"] = func() {
|
||||
cmd := exec.Command("clear")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Run()
|
||||
}
|
||||
clear["windows"] = func() {
|
||||
cmd := exec.Command("cmd", "/c", "cls")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Run()
|
||||
}
|
||||
}
|
||||
|
||||
//Clear simply clears the command line interface (os.Stdout only).
|
||||
func Clear() {
|
||||
value, ok := clear[runtime.GOOS]
|
||||
if ok {
|
||||
value()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package wmenu
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestClear(t *testing.T) {
|
||||
Clear()
|
||||
}
|
||||
|
||||
func TestClearLinux(t *testing.T) {
|
||||
clearOs("linux")
|
||||
}
|
||||
|
||||
func TestClearDarwin(t *testing.T) {
|
||||
clearOs("darwin")
|
||||
}
|
||||
|
||||
func TestClearWindows(t *testing.T) {
|
||||
clearOs("windows")
|
||||
}
|
||||
|
||||
func clearOs(os string) {
|
||||
value, ok := clear[os]
|
||||
if ok {
|
||||
value()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package wmenu
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
//ErrInvalid is returned if a response from user was an invalid option
|
||||
ErrInvalid = errors.New("Invalid response")
|
||||
|
||||
//ErrTooMany is returned if multiSelect is false and a user tries to select multiple options
|
||||
ErrTooMany = errors.New("Too many responses")
|
||||
|
||||
//ErrNoResponse is returned if there were no responses and no action to call
|
||||
ErrNoResponse = errors.New("No response")
|
||||
|
||||
//ErrDuplicate is returned is a user selects an option twice
|
||||
ErrDuplicate = errors.New("Duplicated response")
|
||||
)
|
||||
|
||||
//MenuError records menu errors
|
||||
type MenuError struct {
|
||||
Err error
|
||||
Res string
|
||||
TriesLeft int
|
||||
}
|
||||
|
||||
//Error prints the error in an easy to read string.
|
||||
func (e *MenuError) Error() string {
|
||||
if e.Res != "" {
|
||||
return e.Err.Error() + ": " + e.Res
|
||||
}
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
func newMenuError(err error, res string, tries int) *MenuError {
|
||||
return &MenuError{
|
||||
Err: err,
|
||||
Res: res,
|
||||
TriesLeft: tries,
|
||||
}
|
||||
}
|
||||
|
||||
//IsInvalidErr checks to see if err is of type invalid error returned by menu.
|
||||
func IsInvalidErr(err error) bool {
|
||||
e, ok := err.(*MenuError)
|
||||
if ok && e.Err == ErrInvalid {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//IsNoResponseErr checks to see if err is of type no response returned by menu.
|
||||
func IsNoResponseErr(err error) bool {
|
||||
e, ok := err.(*MenuError)
|
||||
if ok && e.Err == ErrNoResponse {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//IsTooManyErr checks to see if err is of type too many returned by menu.
|
||||
func IsTooManyErr(err error) bool {
|
||||
e, ok := err.(*MenuError)
|
||||
if ok && e.Err == ErrTooMany {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//IsDuplicateErr checks to see if err is of type duplicate returned by menu.
|
||||
func IsDuplicateErr(err error) bool {
|
||||
e, ok := err.(*MenuError)
|
||||
if ok && e.Err == ErrDuplicate {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//IsMenuErr checks to see if it is a menu err.
|
||||
//This is a general check not a specific one.
|
||||
func IsMenuErr(err error) bool {
|
||||
_, ok := err.(*MenuError)
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package wmenu
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
testInvalid = newMenuError(ErrInvalid, "1", 0)
|
||||
testDuplicate = newMenuError(ErrDuplicate, "2", 0)
|
||||
testTooMany = newMenuError(ErrTooMany, "", 0)
|
||||
testNoResponse = newMenuError(ErrNoResponse, "", 0)
|
||||
errNormal = errors.New("Opps")
|
||||
errMenu = newMenuError(errors.New("General"), "", 0)
|
||||
)
|
||||
|
||||
func TestIsInvalidErr(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
assert.True(IsInvalidErr(testInvalid))
|
||||
assert.False(IsInvalidErr(testDuplicate))
|
||||
assert.False(IsInvalidErr(errNormal))
|
||||
}
|
||||
|
||||
func TestIsDuplicateErr(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
assert.True(IsDuplicateErr(testDuplicate))
|
||||
assert.False(IsDuplicateErr(testInvalid))
|
||||
assert.False(IsDuplicateErr(errNormal))
|
||||
}
|
||||
|
||||
func TestIsTooManyErr(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
assert.True(IsTooManyErr(testTooMany))
|
||||
assert.False(IsTooManyErr(testDuplicate))
|
||||
assert.False(IsTooManyErr(errNormal))
|
||||
}
|
||||
|
||||
func TestIsNoResponseErr(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
assert.True(IsNoResponseErr(testNoResponse))
|
||||
assert.False(IsNoResponseErr(testDuplicate))
|
||||
assert.False(IsNoResponseErr(errNormal))
|
||||
}
|
||||
|
||||
func TestIsMenuError(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
assert.True(IsMenuErr(testNoResponse))
|
||||
assert.True(IsMenuErr(testDuplicate))
|
||||
assert.True(IsMenuErr(testInvalid))
|
||||
assert.True(IsMenuErr(testTooMany))
|
||||
assert.True(IsMenuErr(testNoResponse))
|
||||
assert.True(IsMenuErr(errMenu))
|
||||
assert.False(IsMenuErr(errNormal))
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//package main is to show how multiple menus can be used with eachother
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/dixonwille/wmenu"
|
||||
)
|
||||
|
||||
type menuItem int
|
||||
|
||||
const (
|
||||
pizza menuItem = iota
|
||||
iceCream
|
||||
tacos
|
||||
)
|
||||
|
||||
var menuItemStrings = map[menuItem]string{
|
||||
pizza: "Pizza",
|
||||
iceCream: "Ice Cream",
|
||||
tacos: "Tacos",
|
||||
}
|
||||
|
||||
func main() {
|
||||
mm := mainMenu()
|
||||
err := mm.Run()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func mainMenu() *wmenu.Menu {
|
||||
menu := wmenu.NewMenu("What is your favorite food?")
|
||||
menu.Option(menuItemStrings[pizza], pizza, true, nil)
|
||||
menu.Option(menuItemStrings[iceCream], iceCream, false, nil)
|
||||
menu.Option(menuItemStrings[tacos], tacos, false, func(opt wmenu.Opt) error {
|
||||
fmt.Printf("Tacos are great!\n")
|
||||
return nil
|
||||
})
|
||||
menu.Action(func(opts []wmenu.Opt) error {
|
||||
if len(opts) != 1 {
|
||||
return errors.New("wrong number of options chosen")
|
||||
}
|
||||
|
||||
tm := toppingsMenu(opts[0].Value.(menuItem))
|
||||
return tm.Run()
|
||||
})
|
||||
return menu
|
||||
}
|
||||
|
||||
func toppingsMenu(favorite menuItem) *wmenu.Menu {
|
||||
menu := wmenu.NewMenu(fmt.Sprintf("What is your favorite topping for %s?", menuItemStrings[favorite]))
|
||||
if favorite == pizza {
|
||||
menu.Option("Meat", nil, true, nil)
|
||||
menu.Option("Cheese", nil, false, nil)
|
||||
menu.Option("Vegitables", nil, false, nil)
|
||||
}
|
||||
if favorite == iceCream {
|
||||
menu.Option("Fruit", nil, true, nil)
|
||||
menu.Option("Chocolate Syrup", nil, false, nil)
|
||||
menu.Option("Caramel Syrup", nil, false, nil)
|
||||
}
|
||||
menu.Action(func(opts []wmenu.Opt) error {
|
||||
if len(opts) != 1 {
|
||||
return errors.New("wrong number of options chosen")
|
||||
}
|
||||
|
||||
fmt.Printf("Your favorite food is %s with %s on top.\n", menuItemStrings[favorite], opts[0].Text)
|
||||
|
||||
return nil
|
||||
})
|
||||
return menu
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
//Package wmenu creates menus for cli programs.
|
||||
//It uses wlog for it's interface with the command line.
|
||||
//It uses os.Stdin, os.Stdout, and os.Stderr with concurrency by default.
|
||||
//wmenu allows you to change the color of the different parts of the menu.
|
||||
//This package also creates it's own error structure so you can type assert if you need to.
|
||||
//wmenu will validate all responses before calling any function.
|
||||
//It will also figure out which function should be called so you don't have to.
|
||||
package wmenu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
wlog "gopkg.in/dixonwille/wlog.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
y = iota
|
||||
n
|
||||
)
|
||||
|
||||
var (
|
||||
NoColor = os.Getenv("TERM") == "dumb" ||
|
||||
(!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()))
|
||||
)
|
||||
|
||||
//Menu is used to display options to a user.
|
||||
//A user can then select options and Menu will validate the response and perform the correct action.
|
||||
type Menu struct {
|
||||
question string
|
||||
function func([]Opt) error
|
||||
options []Opt
|
||||
ui wlog.UI
|
||||
multiSeparator string
|
||||
allowMultiple bool
|
||||
loopOnInvalid bool
|
||||
clear bool
|
||||
tries int
|
||||
defIcon string
|
||||
isYN bool
|
||||
ynDef int
|
||||
}
|
||||
|
||||
//NewMenu creates a menu with a wlog.UI as the writer.
|
||||
func NewMenu(question string) *Menu {
|
||||
//Create a default ui to use for menu
|
||||
var ui wlog.UI
|
||||
ui = wlog.New(os.Stdin, os.Stdout, os.Stderr)
|
||||
ui = wlog.AddConcurrent(ui)
|
||||
|
||||
return &Menu{
|
||||
question: question,
|
||||
function: nil,
|
||||
options: nil,
|
||||
ui: ui,
|
||||
multiSeparator: " ",
|
||||
allowMultiple: false,
|
||||
loopOnInvalid: false,
|
||||
clear: false,
|
||||
tries: 3,
|
||||
defIcon: "*",
|
||||
isYN: false,
|
||||
ynDef: 0,
|
||||
}
|
||||
}
|
||||
|
||||
//AddColor will change the color of the menu items.
|
||||
//optionColor changes the color of the options.
|
||||
//questionColor changes the color of the questions.
|
||||
//errorColor changes the color of the question.
|
||||
//Use wlog.None if you do not want to change the color.
|
||||
func (m *Menu) AddColor(optionColor, questionColor, responseColor, errorColor wlog.Color) {
|
||||
if !NoColor {
|
||||
m.ui = wlog.AddColor(questionColor, errorColor, wlog.None, wlog.None, optionColor, responseColor, wlog.None, wlog.None, wlog.None, m.ui)
|
||||
}
|
||||
}
|
||||
|
||||
//ClearOnMenuRun will clear the screen when a menu is ran.
|
||||
//This is checked when LoopOnInvalid is activated.
|
||||
//Meaning if an error occurred then it will clear the screen before asking again.
|
||||
func (m *Menu) ClearOnMenuRun() {
|
||||
m.clear = true
|
||||
}
|
||||
|
||||
//SetSeparator sets the separator to use when multiple options are valid responses.
|
||||
//Default value is a space.
|
||||
func (m *Menu) SetSeparator(sep string) {
|
||||
m.multiSeparator = sep
|
||||
}
|
||||
|
||||
//SetTries sets the number of tries on the loop before failing out.
|
||||
//Default is 3.
|
||||
//Negative values act like 0.
|
||||
func (m *Menu) SetTries(i int) {
|
||||
m.tries = i
|
||||
}
|
||||
|
||||
//LoopOnInvalid is used if an invalid option was given then it will prompt the user again.
|
||||
func (m *Menu) LoopOnInvalid() {
|
||||
m.loopOnInvalid = true
|
||||
}
|
||||
|
||||
//SetDefaultIcon sets the icon used to identify which options will be selected by default
|
||||
func (m *Menu) SetDefaultIcon(icon string) {
|
||||
m.defIcon = icon
|
||||
}
|
||||
|
||||
//IsYesNo sets the menu to a yes/no state.
|
||||
//Does not show options but does ask question.
|
||||
//Will also parse the answer to allow for all variants of yes/no (IE Y yes No ...)
|
||||
//Specify the default value using def. 0 is for yes and 1 is for no.
|
||||
//Both will call the Action function you specified.
|
||||
// Opt{ID: 0, Text: "y"} for yes and Opt{ID: 1, Text: "n"} for no will be passed to the function.
|
||||
func (m *Menu) IsYesNo(def int) {
|
||||
m.isYN = true
|
||||
m.ynDef = def
|
||||
}
|
||||
|
||||
//Option adds an option to the menu for the user to select from.
|
||||
//value is an empty interface that can be used to pass anything through to the function.
|
||||
//title is the string the user will select
|
||||
//isDefault is whether this option is a default option (IE when no options are selected).
|
||||
//function is what is called when only this option is selected.
|
||||
//If function is nil then it will default to the menu's Action.
|
||||
func (m *Menu) Option(title string, value interface{}, isDefault bool, function func(Opt) error) {
|
||||
option := newOption(len(m.options), title, value, isDefault, function)
|
||||
m.options = append(m.options, *option)
|
||||
}
|
||||
|
||||
//Action adds a default action to use in certain scenarios.
|
||||
//If the selected option (by default or user selected) does not have a function applied to it this will be called.
|
||||
//If there are no default options and no option was selected this will be called with an option that has an ID of -1.
|
||||
func (m *Menu) Action(function func([]Opt) error) {
|
||||
m.function = function
|
||||
}
|
||||
|
||||
//AllowMultiple will tell the menu to allow multiple selections.
|
||||
//The menu will fail if this is not called and mulple selections were selected.
|
||||
func (m *Menu) AllowMultiple() {
|
||||
m.allowMultiple = true
|
||||
}
|
||||
|
||||
//ChangeReaderWriter changes where the menu listens and writes to.
|
||||
//reader is where user input is collected.
|
||||
//writer and errorWriter is where the menu should write to.
|
||||
func (m *Menu) ChangeReaderWriter(reader io.Reader, writer, errorWriter io.Writer) {
|
||||
var ui wlog.UI
|
||||
ui = wlog.New(reader, writer, errorWriter)
|
||||
m.ui = ui
|
||||
}
|
||||
|
||||
//Run is used to execute the menu.
|
||||
//It will print to options and question to the screen.
|
||||
//It will only clear the screen if ClearOnMenuRun is activated.
|
||||
//This will validate all responses.
|
||||
//Errors are of type MenuError.
|
||||
func (m *Menu) Run() error {
|
||||
if m.clear {
|
||||
Clear()
|
||||
}
|
||||
valid := false
|
||||
var options []Opt
|
||||
//Loop and on error check if loopOnInvalid is enabled.
|
||||
//If it is Clear the screen and write error.
|
||||
//Then ask again
|
||||
for !valid {
|
||||
//step 1 print options to screen
|
||||
m.print()
|
||||
//step 2 ask question, get and validate response
|
||||
opt, err := m.ask()
|
||||
if err != nil {
|
||||
m.tries = m.tries - 1
|
||||
if !IsMenuErr(err) {
|
||||
err = newMenuError(err, "", m.triesLeft())
|
||||
}
|
||||
if m.loopOnInvalid && m.tries > 0 {
|
||||
if m.clear {
|
||||
Clear()
|
||||
}
|
||||
m.ui.Error(err.Error())
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
options = opt
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
//step 3 call appropriate action with the responses
|
||||
return m.callAppropriate(options)
|
||||
}
|
||||
|
||||
func (m *Menu) callAppropriate(options []Opt) (err error) {
|
||||
if len(options) == 0 {
|
||||
return m.callAppropriateNoOptions()
|
||||
}
|
||||
if len(options) == 1 && options[0].function != nil {
|
||||
return options[0].function(options[0])
|
||||
}
|
||||
return m.function(options)
|
||||
}
|
||||
|
||||
func (m *Menu) callAppropriateNoOptions() (err error) {
|
||||
options := m.getDefault()
|
||||
if len(options) == 0 {
|
||||
return m.function([]Opt{{ID: -1}})
|
||||
}
|
||||
if len(options) == 1 && options[0].function != nil {
|
||||
return options[0].function(options[0])
|
||||
}
|
||||
return m.function(options)
|
||||
}
|
||||
|
||||
//hide options when this is a yes or no
|
||||
func (m *Menu) print() {
|
||||
if !m.isYN {
|
||||
for _, opt := range m.options {
|
||||
icon := m.defIcon
|
||||
if !opt.isDefault {
|
||||
icon = ""
|
||||
}
|
||||
m.ui.Output(fmt.Sprintf("%d) %s%s", opt.ID, icon, opt.Text))
|
||||
}
|
||||
} else {
|
||||
//TODO Allow user to specify what to use as value for YN options
|
||||
m.options = []Opt{}
|
||||
m.Option("y", "yes", m.ynDef == y, nil)
|
||||
m.Option("n", "no", m.ynDef == n, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Menu) ask() ([]Opt, error) {
|
||||
if m.isYN {
|
||||
if m.ynDef == y {
|
||||
m.question += " (Y/n)"
|
||||
} else {
|
||||
m.question += " (y/N)"
|
||||
}
|
||||
}
|
||||
trim := ""
|
||||
if m.multiSeparator == " " {
|
||||
trim = m.multiSeparator
|
||||
} else {
|
||||
trim = m.multiSeparator + " "
|
||||
}
|
||||
res, err := m.ui.Ask(m.question, trim)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//Validate responses
|
||||
//Check if no responses are returned and no action to call
|
||||
if res == "" {
|
||||
//get default options
|
||||
opt := m.getDefault()
|
||||
if !m.validOptAndFunc(opt) {
|
||||
return nil, newMenuError(ErrNoResponse, "", m.triesLeft())
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var responses []int
|
||||
if !m.isYN {
|
||||
responses, err = m.resToInt(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = m.validateResponses(responses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
responses, err = m.ynResParse(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
//Parse responses and return them as options
|
||||
var finalOptions []Opt
|
||||
for _, response := range responses {
|
||||
finalOptions = append(finalOptions, m.options[response])
|
||||
}
|
||||
|
||||
return finalOptions, nil
|
||||
}
|
||||
|
||||
//Converts the response string to a slice of ints, also validates along the way.
|
||||
func (m *Menu) resToInt(res string) ([]int, error) {
|
||||
resStrings := strings.Split(res, m.multiSeparator)
|
||||
//Check if we don't want multiple responses
|
||||
if !m.allowMultiple && len(resStrings) > 1 {
|
||||
return nil, newMenuError(ErrTooMany, "", m.triesLeft())
|
||||
}
|
||||
|
||||
//Convert responses to intigers
|
||||
var responses []int
|
||||
for _, response := range resStrings {
|
||||
//Check if it is an intiger
|
||||
response = strings.Trim(response, " ")
|
||||
r, err := strconv.Atoi(response)
|
||||
if err != nil {
|
||||
return nil, newMenuError(ErrInvalid, response, m.triesLeft())
|
||||
}
|
||||
responses = append(responses, r)
|
||||
}
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (m *Menu) ynResParse(res string) ([]int, error) {
|
||||
resStrings := strings.Split(res, m.multiSeparator)
|
||||
if len(resStrings) > 1 {
|
||||
return nil, newMenuError(ErrTooMany, "", m.triesLeft())
|
||||
}
|
||||
re := regexp.MustCompile("^\\s*(?:([Yy])(?:es|ES)?|([Nn])(?:o|O)?)\\s*$")
|
||||
matches := re.FindStringSubmatch(res)
|
||||
if len(matches) < 2 {
|
||||
return nil, newMenuError(ErrInvalid, res, m.triesLeft())
|
||||
}
|
||||
if strings.ToLower(matches[1]) == "y" {
|
||||
return []int{y}, nil
|
||||
}
|
||||
return []int{n}, nil
|
||||
}
|
||||
|
||||
//Check if response is in the range of options
|
||||
//If it is make sure it is not duplicated
|
||||
func (m *Menu) validateResponses(responses []int) error {
|
||||
var tmp []int
|
||||
for _, response := range responses {
|
||||
if response < 0 || len(m.options)-1 < response {
|
||||
return newMenuError(ErrInvalid, strconv.Itoa(response), m.triesLeft())
|
||||
}
|
||||
|
||||
if exist(tmp, response) {
|
||||
return newMenuError(ErrDuplicate, strconv.Itoa(response), m.triesLeft())
|
||||
}
|
||||
|
||||
tmp = append(tmp, response)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//Simply checks if number exists in the slice
|
||||
func exist(slice []int, number int) bool {
|
||||
for _, s := range slice {
|
||||
if number == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//gets a list of default options
|
||||
func (m *Menu) getDefault() []Opt {
|
||||
var opt []Opt
|
||||
for _, o := range m.options {
|
||||
if o.isDefault {
|
||||
opt = append(opt, o)
|
||||
}
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
//make sure that there is an action available to be called in certain cases
|
||||
//returns false if it chould not find an action for the number options available
|
||||
func (m *Menu) validOptAndFunc(opt []Opt) bool {
|
||||
if m.function == nil {
|
||||
if len(opt) == 1 && opt[0].function != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Menu) triesLeft() int {
|
||||
if m.loopOnInvalid && m.tries > 0 {
|
||||
return m.tries
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,710 @@
|
||||
package wmenu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
wlog "gopkg.in/dixonwille/wlog.v2"
|
||||
)
|
||||
|
||||
var newMenuCases = []string{"Testing this menu.", "", "!@#$%^&*()"}
|
||||
var setSeperatorCases = []string{"", ".", ",", "~"}
|
||||
var defaultIconCases = []string{"", ",", "*", "~", "!", "@"}
|
||||
var setTriesCases = []int{0, -4, 5}
|
||||
var optionCases = []struct {
|
||||
name string
|
||||
value interface{}
|
||||
def bool
|
||||
function func(Opt) error
|
||||
}{
|
||||
{"Options", "Options", true, func(Opt) error { fmt.Println("testing option"); return nil }},
|
||||
{"", "Options", true, func(Opt) error { fmt.Println("testing option"); return nil }},
|
||||
{"Options", "", false, func(Opt) error { fmt.Println("testing option"); return nil }},
|
||||
{"", "", false, nil},
|
||||
{"", nil, false, nil},
|
||||
}
|
||||
var actionCases = []func([]Opt) error{
|
||||
func(opts []Opt) error { fmt.Println(opts); return nil },
|
||||
nil,
|
||||
}
|
||||
var addColorCases = []struct {
|
||||
opt wlog.Color
|
||||
que wlog.Color
|
||||
res wlog.Color
|
||||
err wlog.Color
|
||||
}{
|
||||
{wlog.None, wlog.None, wlog.None, wlog.None},
|
||||
{wlog.Red, wlog.Green, wlog.Blue, wlog.Black},
|
||||
{wlog.Yellow, wlog.White, wlog.Cyan, wlog.Magenta},
|
||||
{wlog.BrightBlack, wlog.BrightBlue, wlog.BrightCyan, wlog.BrightGreen},
|
||||
{wlog.BrightRed, wlog.BrightWhite, wlog.BrightYellow, wlog.BrightMagenta},
|
||||
}
|
||||
var errorCases = []struct {
|
||||
input string
|
||||
optFunction func(option Opt) error
|
||||
defFunction func([]Opt) error
|
||||
expected string
|
||||
singDef bool
|
||||
multiDef bool
|
||||
}{
|
||||
{"0\r\n", func(option Opt) error { return errors.New("Oops") }, nil, "Oops", false, false},
|
||||
{"0\r\n", nil, func(opts []Opt) error { return errors.New("Oops") }, "Oops", false, false},
|
||||
{"0 1\r\n", nil, func(opts []Opt) error { return errors.New("Oops") }, "Too many responses", false, false},
|
||||
{"\r\n", func(option Opt) error { return errors.New("Oops") }, nil, "Oops", true, false},
|
||||
{"\r\n", nil, func(opts []Opt) error { return errors.New("Oops") }, "Oops", true, false},
|
||||
{"\r\n", nil, func(opts []Opt) error { return errors.New("Oops") }, "Oops", false, false},
|
||||
{"\r\n", nil, func(opts []Opt) error { return errors.New("Oops") }, "Oops", true, true},
|
||||
}
|
||||
var ynCases = []struct {
|
||||
input string
|
||||
defFunc func([]Opt) error
|
||||
expected string
|
||||
def int
|
||||
}{
|
||||
{"\r\n", func(opts []Opt) error { return errors.New(opts[0].Text) }, "y", 0},
|
||||
{"\r\n", func(opts []Opt) error { return errors.New(opts[0].Text) }, "n", 1},
|
||||
{"YES\r\n", func(opts []Opt) error { return errors.New(opts[0].Text) }, "y", 0},
|
||||
{"n\r\n", func(opts []Opt) error { return errors.New(opts[0].Text) }, "n", 0},
|
||||
{" Yes \r\n", func(opts []Opt) error { return errors.New(opts[0].Text) }, "y", 0},
|
||||
{"ahh\r\n", func(opts []Opt) error { return errors.New(opts[0].Text) }, "Invalid response: ahh", 0},
|
||||
{"boo ahh\r\n", func(opts []Opt) error { return errors.New(opts[0].Text) }, "Too many responses", 0},
|
||||
}
|
||||
var trimCases = []struct {
|
||||
input string
|
||||
del string
|
||||
expected string
|
||||
}{
|
||||
{"0 \r\n", " ", "0\r\n"},
|
||||
{" 0\r\n", " ", "0\r\n"},
|
||||
{" 0 \r\n", " ", "0\r\n"},
|
||||
{"0 1 \r\n", " ", "0 1\r\n"},
|
||||
{" 0 1\r\n", " ", "0 1\r\n"},
|
||||
{" 0 1 \r\n", " ", "0 1\r\n"},
|
||||
{" 0, 1 \r\n", ",", "0 1\r\n"},
|
||||
{"0, 1,\r\n", ",", "0 1\r\n"},
|
||||
{"0, 1, \r\n", ",", "0 1\r\n"},
|
||||
}
|
||||
|
||||
func Example_optionValue() {
|
||||
type NameEntity struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
}
|
||||
|
||||
reader := strings.NewReader("1\r\n") //Simulates the user typing "1" and hitting the [enter] key
|
||||
optFunc := func(option Opt) error {
|
||||
fmt.Println("Option 0 was chosen.")
|
||||
return nil
|
||||
}
|
||||
actFunc := func(opts []Opt) error {
|
||||
name, ok := opts[0].Value.(NameEntity)
|
||||
if !ok {
|
||||
log.Fatal("Could not cast option's value to NameEntity")
|
||||
}
|
||||
fmt.Printf("%s has an id of %d.\n", opts[0].Text, opts[0].ID)
|
||||
fmt.Printf("Hello, %s %s.\n", name.FirstName, name.LastName)
|
||||
return nil
|
||||
}
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, os.Stdout, os.Stderr)
|
||||
menu.Action(actFunc)
|
||||
menu.Option("Option 0", NameEntity{"Bill", "Bob"}, true, optFunc)
|
||||
menu.Option("Option 1", NameEntity{"John", "Doe"}, false, nil)
|
||||
menu.Option("Option 2", NameEntity{"Jane", "Doe"}, false, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
//Output:
|
||||
//0) *Option 0
|
||||
//1) Option 1
|
||||
//2) Option 2
|
||||
//Choose an option.
|
||||
//Option 1 has an id of 1.
|
||||
//Hello, John Doe.
|
||||
}
|
||||
|
||||
func Example_yesNo() {
|
||||
reader := strings.NewReader("y\r\n") //Simulates the user typing "y" and hitting the [enter] key
|
||||
actFunc := func(opts []Opt) error {
|
||||
fmt.Printf("%s has an id of %d.\n", opts[0].Text, opts[0].ID)
|
||||
fmt.Printf("But has a value of %s.\n", opts[0].Value.(string))
|
||||
return nil
|
||||
}
|
||||
menu := NewMenu("Would you like to start?")
|
||||
menu.ChangeReaderWriter(reader, os.Stdout, os.Stderr)
|
||||
menu.Action(actFunc)
|
||||
menu.IsYesNo(0)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
//Output:
|
||||
//Would you like to start? (Y/n)
|
||||
//y has an id of 0.
|
||||
//But has a value of yes.
|
||||
}
|
||||
|
||||
func Example_simpleDefault() {
|
||||
reader := strings.NewReader("\r\n") //Simulates the user hitting the [enter] key
|
||||
optFunc := func(option Opt) error {
|
||||
fmt.Fprint(os.Stdout, "Option 0 was chosen.")
|
||||
return nil
|
||||
}
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, os.Stdout, os.Stderr)
|
||||
menu.Option("Option 0", "0", true, optFunc)
|
||||
menu.Option("Option 1", "1", false, nil)
|
||||
menu.Option("Option 2", "2", false, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
//Output:
|
||||
//0) *Option 0
|
||||
//1) Option 1
|
||||
//2) Option 2
|
||||
//Choose an option.
|
||||
//Option 0 was chosen.
|
||||
}
|
||||
|
||||
func Example_multiple() {
|
||||
reader := strings.NewReader("1,2\r\n") //Simulates the user typing "1,2" and hitting the [enter] key
|
||||
optFunc := func(option Opt) error {
|
||||
fmt.Println("Option 0 was chosen.")
|
||||
return nil
|
||||
}
|
||||
actFunc := func(opts []Opt) error {
|
||||
for _, opt := range opts {
|
||||
fmt.Printf("%s has an id of %d.\n", opt.Text, opt.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, os.Stdout, os.Stderr)
|
||||
menu.Action(actFunc)
|
||||
menu.AllowMultiple()
|
||||
menu.SetSeparator(",")
|
||||
menu.Option("Option 0", "0", true, optFunc)
|
||||
menu.Option("Option 1", "1", false, nil)
|
||||
menu.Option("Option 2", "2", true, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
//Output:
|
||||
//0) *Option 0
|
||||
//1) Option 1
|
||||
//2) *Option 2
|
||||
//Choose an option.
|
||||
//Option 1 has an id of 1.
|
||||
//Option 2 has an id of 2.
|
||||
}
|
||||
|
||||
func Example_multipleDefault() {
|
||||
reader := strings.NewReader("\r\n") //Simulates the user hitting the [enter] key
|
||||
optFunc := func(option Opt) error {
|
||||
fmt.Println("Option 0 was chosen.")
|
||||
return nil
|
||||
}
|
||||
actFunc := func(opts []Opt) error {
|
||||
for _, opt := range opts {
|
||||
fmt.Printf("%s has an id of %d.\n", opt.Text, opt.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, os.Stdout, os.Stderr)
|
||||
menu.Action(actFunc)
|
||||
menu.AllowMultiple()
|
||||
menu.Option("Option 0", "0", true, optFunc)
|
||||
menu.Option("Option 1", "1", false, nil)
|
||||
menu.Option("Option 2", "2", true, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
//Output:
|
||||
//0) *Option 0
|
||||
//1) Option 1
|
||||
//2) *Option 2
|
||||
//Choose an option.
|
||||
//Option 0 has an id of 0.
|
||||
//Option 2 has an id of 2.
|
||||
}
|
||||
|
||||
func Example_errorNoResponse() {
|
||||
reader := strings.NewReader("\r\n") //Simulates the user hitting the [enter] key
|
||||
optFunc := func(option Opt) error {
|
||||
fmt.Println("Option 0 was chosen.")
|
||||
return nil
|
||||
}
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, os.Stdout, os.Stderr)
|
||||
menu.Option("Option 0", "0", false, optFunc)
|
||||
menu.Option("Option 1", "1", false, nil)
|
||||
menu.Option("Option 2", "2", false, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
if IsNoResponseErr(err) {
|
||||
fmt.Println("We caught the err: " + err.Error())
|
||||
} else {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
//Output:
|
||||
//0) Option 0
|
||||
//1) Option 1
|
||||
//2) Option 2
|
||||
//Choose an option.
|
||||
//We caught the err: No response
|
||||
}
|
||||
|
||||
func Example_errorInvalid() {
|
||||
reader := strings.NewReader("3\r\n") //Simulates the user typing "3" and hitting the [enter] key
|
||||
optFunc := func(option Opt) error {
|
||||
fmt.Println("Option 0 was chosen.")
|
||||
return nil
|
||||
}
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, os.Stdout, os.Stderr)
|
||||
menu.Option("Option 0", "0", false, optFunc)
|
||||
menu.Option("Option 1", "1", false, nil)
|
||||
menu.Option("Option 2", "2", false, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
if IsInvalidErr(err) {
|
||||
fmt.Println("We caught the err: " + err.Error())
|
||||
} else {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
//Output:
|
||||
//0) Option 0
|
||||
//1) Option 1
|
||||
//2) Option 2
|
||||
//Choose an option.
|
||||
//We caught the err: Invalid response: 3
|
||||
}
|
||||
|
||||
func Example_errorTooMany() {
|
||||
reader := strings.NewReader("1 2\r\n") //Simulates the user typing "1 2" and hitting the [enter] key
|
||||
optFunc := func(option Opt) error {
|
||||
fmt.Println("Option 0 was chosen.")
|
||||
return nil
|
||||
}
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, os.Stdout, os.Stderr)
|
||||
menu.Option("Option 0", "0", false, optFunc)
|
||||
menu.Option("Option 1", "1", false, nil)
|
||||
menu.Option("Option 2", "2", false, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
if IsTooManyErr(err) {
|
||||
fmt.Println("We caught the err: " + err.Error())
|
||||
} else {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
//Output:
|
||||
//0) Option 0
|
||||
//1) Option 1
|
||||
//2) Option 2
|
||||
//Choose an option.
|
||||
//We caught the err: Too many responses
|
||||
}
|
||||
|
||||
func Example_errorDuplicate() {
|
||||
reader := strings.NewReader("1 1\r\n") //Simulates the user typing "1 1" and hitting the [enter] key
|
||||
optFunc := func(option Opt) error {
|
||||
fmt.Println("Option 0 was chosen.")
|
||||
return nil
|
||||
}
|
||||
actFunc := func(opts []Opt) error {
|
||||
for _, opt := range opts {
|
||||
fmt.Printf("%s has an id of %d.\n", opt.Text, opt.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, os.Stdout, os.Stderr)
|
||||
menu.Action(actFunc)
|
||||
menu.AllowMultiple()
|
||||
menu.Option("Option 0", "0", false, optFunc)
|
||||
menu.Option("Option 1", "1", false, nil)
|
||||
menu.Option("Option 2", "2", false, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
if IsDuplicateErr(err) {
|
||||
fmt.Println("We caught the err: " + err.Error())
|
||||
} else {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
//Output:
|
||||
//0) Option 0
|
||||
//1) Option 1
|
||||
//2) Option 2
|
||||
//Choose an option.
|
||||
//We caught the err: Duplicated response: 1
|
||||
|
||||
}
|
||||
|
||||
func TestNewMenu(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
for _, c := range newMenuCases {
|
||||
menu := NewMenu(c)
|
||||
assert.Equal(c, menu.question)
|
||||
assert.Nil(menu.function)
|
||||
assert.Nil(menu.options)
|
||||
assert.Equal(" ", menu.multiSeparator)
|
||||
assert.False(menu.allowMultiple)
|
||||
assert.False(menu.loopOnInvalid)
|
||||
assert.False(menu.clear)
|
||||
assert.NotNil(menu.ui)
|
||||
assert.Equal(3, menu.tries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearOnMenuRun(t *testing.T) {
|
||||
menu := NewMenu("Testing")
|
||||
menu.ClearOnMenuRun()
|
||||
assert.True(t, menu.clear)
|
||||
}
|
||||
|
||||
func TestSetSeperator(t *testing.T) {
|
||||
menu := NewMenu("Testing")
|
||||
for _, c := range setSeperatorCases {
|
||||
menu.SetSeparator(c)
|
||||
assert.Equal(t, c, menu.multiSeparator)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetTries(t *testing.T) {
|
||||
menu := NewMenu("Testing")
|
||||
for _, c := range setTriesCases {
|
||||
menu.SetTries(c)
|
||||
assert.Equal(t, c, menu.tries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoopOnInvalid(t *testing.T) {
|
||||
menu := NewMenu("Testing")
|
||||
menu.LoopOnInvalid()
|
||||
assert.True(t, menu.loopOnInvalid)
|
||||
}
|
||||
|
||||
func TestOption(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
menu := NewMenu("Testing")
|
||||
for i, c := range optionCases {
|
||||
menu.Option(c.name, c.value, c.def, c.function)
|
||||
require.Equal(t, i+1, len(menu.options))
|
||||
assert.Equal(i, menu.options[i].ID)
|
||||
assert.Equal(c.name, menu.options[i].Text)
|
||||
assert.Equal(c.def, menu.options[i].isDefault)
|
||||
assert.Equal(c.value, menu.options[i].Value)
|
||||
if c.function != nil {
|
||||
assert.NotNil(menu.options[i].function)
|
||||
} else {
|
||||
assert.Nil(menu.options[i].function)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAction(t *testing.T) {
|
||||
menu := NewMenu("Testing")
|
||||
for _, c := range actionCases {
|
||||
menu.Action(c)
|
||||
if c != nil {
|
||||
assert.NotNil(t, menu.function)
|
||||
} else {
|
||||
assert.Nil(t, menu.function)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowMultiple(t *testing.T) {
|
||||
menu := NewMenu("Testing")
|
||||
menu.AllowMultiple()
|
||||
assert.True(t, menu.allowMultiple)
|
||||
}
|
||||
|
||||
func TestAddColor(t *testing.T) {
|
||||
menu := NewMenu("Testing")
|
||||
for _, c := range addColorCases {
|
||||
menu.AddColor(c.opt, c.que, c.res, c.err)
|
||||
//Nothing to assert on just make sure the function does not fail
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDefaultIcon(t *testing.T) {
|
||||
menu := NewMenu("Testing")
|
||||
for _, c := range defaultIconCases {
|
||||
menu.SetDefaultIcon(c)
|
||||
assert.Equal(t, c, menu.defIcon)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestClearInAsk(t *testing.T) {
|
||||
stdOut := initTest()
|
||||
reader := strings.NewReader("1\r\n") //Simulates the user typing "1" and hitting the [enter] key
|
||||
optFunc := func(option Opt) error {
|
||||
assert.Fail(t, "Should not have called Option 0's function")
|
||||
return nil
|
||||
}
|
||||
actFunc := func(opts []Opt) error {
|
||||
assert.Len(t, opts, 1)
|
||||
assert.Equal(t, 1, opts[0].ID)
|
||||
assert.Equal(t, "Option 1", opts[0].Text)
|
||||
assert.Nil(t, opts[0].function)
|
||||
assert.False(t, opts[0].isDefault)
|
||||
return nil
|
||||
}
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, stdOut, stdOut)
|
||||
menu.Action(actFunc)
|
||||
menu.ClearOnMenuRun()
|
||||
menu.Option("Option 0", "0", true, optFunc)
|
||||
menu.Option("Option 1", "1", false, nil)
|
||||
menu.Option("Option 2", "2", true, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
assert.Fail(t, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAction(t *testing.T) {
|
||||
stdOut := initTest()
|
||||
reader := strings.NewReader("\r\n") //Simulates the user hitting the [enter] key
|
||||
optFunc := func(option Opt) error {
|
||||
assert.Fail(t, "Should not have called option 0's function")
|
||||
return nil
|
||||
}
|
||||
actFunc := func(opts []Opt) error {
|
||||
assert.Len(t, opts, 1)
|
||||
assert.Equal(t, -1, opts[0].ID)
|
||||
return nil
|
||||
}
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, stdOut, stdOut)
|
||||
menu.Action(actFunc)
|
||||
menu.Option("Option 0", "0", false, optFunc)
|
||||
menu.Option("Option 1", "1", false, nil)
|
||||
menu.Option("Option 2", "2", false, nil)
|
||||
// spew.Dump(menu)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
assert.Fail(t, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultActionWithDefaultOption(t *testing.T) {
|
||||
stdOut := initTest()
|
||||
reader := strings.NewReader("\r\n") //Simulates the user hitting the [enter] key
|
||||
optFunc := func(option Opt) error {
|
||||
assert.Fail(t, "Should not have called option 0's function")
|
||||
return nil
|
||||
}
|
||||
actFunc := func(opts []Opt) error {
|
||||
assert.Len(t, opts, 1)
|
||||
assert.Equal(t, 1, opts[0].ID)
|
||||
assert.Equal(t, "Option 1", opts[0].Text)
|
||||
assert.Nil(t, opts[0].function)
|
||||
assert.True(t, opts[0].isDefault)
|
||||
return nil
|
||||
}
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, stdOut, stdOut)
|
||||
menu.Action(actFunc)
|
||||
menu.Option("Option 0", "0", false, optFunc)
|
||||
menu.Option("Option 1", "1", true, nil)
|
||||
menu.Option("Option 2", "2", false, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
assert.Fail(t, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionsFunction(t *testing.T) {
|
||||
stdOut := initTest()
|
||||
reader := strings.NewReader("0\r\n") //Simulates the user typing "0" and hitting the [enter] key
|
||||
optFunc := func(option Opt) error {
|
||||
return nil
|
||||
}
|
||||
actFunc := func(opts []Opt) error {
|
||||
assert.Fail(t, "Should not have called the menu's default function")
|
||||
return nil
|
||||
}
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, stdOut, stdOut)
|
||||
menu.Action(actFunc)
|
||||
menu.Option("Option 0", "0", false, optFunc)
|
||||
menu.Option("Option 1", "1", true, nil)
|
||||
menu.Option("Option 2", "2", false, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
assert.Fail(t, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWlogAskErr(t *testing.T) {
|
||||
stdOut := initTest()
|
||||
reader := strings.NewReader("1") //Simulates the user typing "1" without hitting [enter]. Can't happen when reader is os.Stdin
|
||||
optFunc := func(option Opt) error {
|
||||
assert.Fail(t, "Should not have called option 0's function")
|
||||
return nil
|
||||
}
|
||||
actFunc := func(opts []Opt) error {
|
||||
assert.Fail(t, "Should not have called the menu's default function")
|
||||
return nil
|
||||
}
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, stdOut, stdOut)
|
||||
menu.Action(actFunc)
|
||||
menu.Option("Option 0", "0", false, optFunc)
|
||||
menu.Option("Option 1", "1", true, nil)
|
||||
menu.Option("Option 2", "2", false, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
assert.Equal(t, "EOF", err.Error())
|
||||
} else {
|
||||
assert.Fail(t, "Expected to get an error of EOF but instead got no error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLetterForResponse(t *testing.T) {
|
||||
stdOut := initTest()
|
||||
reader := strings.NewReader("a\r\n") //Simulates the user typing "a" and hitting [enter].
|
||||
optFunc := func(option Opt) error {
|
||||
assert.Fail(t, "Should not have called option 0's function")
|
||||
return nil
|
||||
}
|
||||
actFunc := func(opts []Opt) error {
|
||||
assert.Fail(t, "Should not have called the menu's default function")
|
||||
return nil
|
||||
}
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, stdOut, stdOut)
|
||||
menu.Action(actFunc)
|
||||
menu.Option("Option 0", "0", false, optFunc)
|
||||
menu.Option("Option 1", "1", true, nil)
|
||||
menu.Option("Option 2", "2", false, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
require.True(t, IsInvalidErr(err))
|
||||
e := err.(*MenuError)
|
||||
assert.Equal(t, "a", e.Res)
|
||||
} else {
|
||||
assert.Fail(t, "Expected to get an Invalid Response error but instead got no error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionError(t *testing.T) {
|
||||
stdout := initTest()
|
||||
for _, c := range errorCases {
|
||||
reader := strings.NewReader(c.input)
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, stdout, stdout)
|
||||
menu.Action(c.defFunction)
|
||||
if c.multiDef {
|
||||
menu.AllowMultiple()
|
||||
}
|
||||
menu.Option("Option 0", "0", c.singDef, c.optFunction)
|
||||
menu.Option("Option 1", "1", false, nil)
|
||||
menu.Option("Option 2", "2", c.multiDef, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
assert.Equal(t, c.expected, err.Error())
|
||||
} else {
|
||||
assert.Fail(t, "Expected an error but did not get one")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestLoopAndTries(t *testing.T) {
|
||||
stdOut := initTest()
|
||||
optFunc := func(option Opt) error {
|
||||
assert.Fail(t, "Should not have called option 0's function")
|
||||
return nil
|
||||
}
|
||||
for _, c := range setTriesCases {
|
||||
reader := strings.NewReader("a") //Simulates the user typing "a" and not hitting [enter].
|
||||
menu := NewMenu("Choose an option.")
|
||||
menu.ChangeReaderWriter(reader, stdOut, stdOut)
|
||||
menu.SetTries(c)
|
||||
menu.LoopOnInvalid()
|
||||
menu.ClearOnMenuRun()
|
||||
menu.Option("Option 0", "0", false, optFunc)
|
||||
menu.Option("Option 1", "1", false, nil)
|
||||
menu.Option("Option 2", "2", false, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
require.True(t, IsMenuErr(err))
|
||||
e := err.(*MenuError)
|
||||
assert.Equal(t, 0, e.TriesLeft)
|
||||
} else {
|
||||
assert.Fail(t, "Expected to get a general Menu error but instead got no error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestYesNo(t *testing.T) {
|
||||
for _, c := range ynCases {
|
||||
stdOut := initTest()
|
||||
reader := strings.NewReader(c.input)
|
||||
menu := NewMenu("Yes or No")
|
||||
menu.ChangeReaderWriter(reader, stdOut, stdOut)
|
||||
menu.IsYesNo(c.def)
|
||||
menu.Action(c.defFunc)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
assert.Equal(t, c.expected, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrim(t *testing.T) {
|
||||
for _, c := range trimCases {
|
||||
stdOut := initTest()
|
||||
actOut := initTest()
|
||||
reader := strings.NewReader(c.input)
|
||||
menu := NewMenu("Testing")
|
||||
menu.ChangeReaderWriter(reader, stdOut, stdOut)
|
||||
menu.SetSeparator(c.del)
|
||||
menu.Action(func(opts []Opt) error {
|
||||
var actual []string
|
||||
for _, opt := range opts {
|
||||
actual = append(actual, opt.Text)
|
||||
}
|
||||
fmt.Fprintf(actOut, "%s\r\n", strings.Join(actual, " "))
|
||||
return nil
|
||||
})
|
||||
menu.AllowMultiple()
|
||||
menu.Option("0", nil, false, nil)
|
||||
menu.Option("1", nil, false, nil)
|
||||
err := menu.Run()
|
||||
if err != nil {
|
||||
assert.Fail(t, err.Error())
|
||||
}
|
||||
act, err := actOut.ReadString((byte)('\n'))
|
||||
if err != nil {
|
||||
assert.Fail(t, err.Error())
|
||||
}
|
||||
assert.Equal(t, c.expected, act)
|
||||
}
|
||||
}
|
||||
|
||||
func initTest() *bytes.Buffer {
|
||||
var b []byte
|
||||
return bytes.NewBuffer(b)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package wmenu
|
||||
|
||||
//Opt is what Menu uses to display options to screen.
|
||||
//Also holds information on what should run and if it is a default option
|
||||
type Opt struct {
|
||||
ID int
|
||||
Text string
|
||||
Value interface{}
|
||||
function func(Opt) error
|
||||
isDefault bool
|
||||
}
|
||||
|
||||
func newOption(id int, text string, value interface{}, def bool, function func(Opt) error) *Opt {
|
||||
return &Opt{
|
||||
ID: id,
|
||||
Text: text,
|
||||
Value: value,
|
||||
isDefault: def,
|
||||
function: function,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user