Error handling. Vendoring

Signed-off-by: Ettore Dreucci <ettore.dreucci@gmail.com>
This commit is contained in:
2018-06-19 00:26:51 +02:00
parent 0c18f8094e
commit cb07e131d9
322 changed files with 175629 additions and 113 deletions

387
vendor/github.com/dixonwille/wmenu/menu.go generated vendored Normal file
View File

@@ -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
}