aboutsummaryrefslogblamecommitdiffstats
path: root/commands/input/prompt.go
blob: 576d45bbe58f0cc0469326a45348b99c2299abe8 (plain) (tree)
1
2
3
4
5
6
7
8
9




               
                 
            
              
                 
                 
                 
              


                                          

                                                         
                                                       

 
                                                  

                                                                                               







                                                                                  

 














                                                                       

          
                                 

                                                                                 

 
                                                             
                                                                                                  
     

                                   
                                                                                    
                        
                                                                     









                                                                       
                                       

                 






                                                                         
                                             
                         




                                
 
                                                                                          
                                                                                         
                                                               




                                                          
                                                                      


                      
     


                                                             
                                                                              
















                                                                                          
                                             






                                

                                                            
















                                                                                
                                                                       


                                
                                     

         


























                                                                                                                     
                                                                       













                                                                            


                                                                                                                           
         

                                                       
         
 


                                         

                                              




                                                                                


                                                      
                                                                                            









                                                                               
                                                   












                                                                              
                                          



                                                
                                                                                     



                                                                       
                        
                                           
                                                  
                        
                                                                        


                 
package input

import (
	"bufio"
	"fmt"
	"net/url"
	"os"
	"sort"
	"strconv"
	"strings"
	"syscall"
	"time"

	"golang.org/x/crypto/ssh/terminal"

	"github.com/MichaelMure/git-bug/bridge/core/auth"
	"github.com/MichaelMure/git-bug/util/colors"
	"github.com/MichaelMure/git-bug/util/interrupt"
)

// PromptValidator is a validator for a user entry
// If complaint is "", value is considered valid, otherwise it's the error reported to the user
// If err != nil, a terminal error happened
type PromptValidator func(name string, value string) (complaint string, err error)

// Required is a validator preventing a "" value
func Required(name string, value string) (string, error) {
	if value == "" {
		return fmt.Sprintf("%s is empty", name), nil
	}
	return "", nil
}

// IsURL is a validator checking that the value is a fully formed URL
func IsURL(name string, value string) (string, error) {
	u, err := url.Parse(value)
	if err != nil {
		return fmt.Sprintf("%s is invalid: %v", name, err), nil
	}
	if u.Scheme == "" {
		return fmt.Sprintf("%s is missing a scheme", name), nil
	}
	if u.Host == "" {
		return fmt.Sprintf("%s is missing a host", name), nil
	}
	return "", nil
}

// Prompts

// Prompt is a simple text input.
func Prompt(prompt, name string, validators ...PromptValidator) (string, error) {
	return PromptDefault(prompt, name, "", validators...)
}

// PromptDefault is a simple text input with a default value.
func PromptDefault(prompt, name, preValue string, validators ...PromptValidator) (string, error) {
loop:
	for {
		if preValue != "" {
			_, _ = fmt.Fprintf(os.Stderr, "%s [%s]: ", prompt, preValue)
		} else {
			_, _ = fmt.Fprintf(os.Stderr, "%s: ", prompt)
		}

		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
		if err != nil {
			return "", err
		}

		line = strings.TrimSpace(line)

		if preValue != "" && line == "" {
			line = preValue
		}

		for _, validator := range validators {
			complaint, err := validator(name, line)
			if err != nil {
				return "", err
			}
			if complaint != "" {
				_, _ = fmt.Fprintln(os.Stderr, complaint)
				continue loop
			}
		}

		return line, nil
	}
}

// PromptPassword is a specialized text input that doesn't display the characters entered.
func PromptPassword(prompt, name string, validators ...PromptValidator) (string, error) {
	termState, err := terminal.GetState(int(syscall.Stdin))
	if err != nil {
		return "", err
	}

	cancel := interrupt.RegisterCleaner(func() error {
		return terminal.Restore(int(syscall.Stdin), termState)
	})
	defer cancel()

loop:
	for {
		_, _ = fmt.Fprintf(os.Stderr, "%s: ", prompt)

		bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
		// new line for coherent formatting, ReadPassword clip the normal new line
		// entered by the user
		fmt.Println()

		if err != nil {
			return "", err
		}

		pass := string(bytePassword)

		for _, validator := range validators {
			complaint, err := validator(name, pass)
			if err != nil {
				return "", err
			}
			if complaint != "" {
				_, _ = fmt.Fprintln(os.Stderr, complaint)
				continue loop
			}
		}

		return pass, nil
	}
}

// PromptChoice is a prompt giving possible choices
// Return the index starting at zero of the choice selected.
func PromptChoice(prompt string, choices []string) (int, error) {
	for {
		for i, choice := range choices {
			_, _ = fmt.Fprintf(os.Stderr, "[%d]: %s\n", i+1, choice)
		}
		_, _ = fmt.Fprintf(os.Stderr, "%s: ", prompt)

		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
		fmt.Println()
		if err != nil {
			return 0, err
		}

		line = strings.TrimSpace(line)

		index, err := strconv.Atoi(line)
		if err != nil || index < 1 || index > len(choices) {
			_, _ = fmt.Fprintln(os.Stderr, "invalid input")
			continue
		}

		return index - 1, nil
	}
}

func PromptURLWithRemote(prompt, name string, validRemotes []string, validators ...PromptValidator) (string, error) {
	if len(validRemotes) == 0 {
		return Prompt(prompt, name, validators...)
	}

	sort.Strings(validRemotes)

	for {
		_, _ = fmt.Fprintln(os.Stderr, "\nDetected projects:")

		for i, remote := range validRemotes {
			_, _ = fmt.Fprintf(os.Stderr, "[%d]: %v\n", i+1, remote)
		}

		_, _ = fmt.Fprintf(os.Stderr, "\n[0]: Another project\n\n")
		_, _ = fmt.Fprintf(os.Stderr, "Select option: ")

		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
		if err != nil {
			return "", err
		}

		line = strings.TrimSpace(line)

		index, err := strconv.Atoi(line)
		if err != nil || index < 0 || index > len(validRemotes) {
			_, _ = fmt.Fprintln(os.Stderr, "invalid input")
			continue
		}

		// if user want to enter another project url break this loop
		if index == 0 {
			break
		}

		return validRemotes[index-1], nil
	}

	return Prompt(prompt, name, validators...)
}

func PromptCredential(target, name string, credentials []auth.Credential, choices []string) (auth.Credential, int, error) {
	if len(credentials) == 0 && len(choices) == 0 {
		return nil, 0, fmt.Errorf("no possible choice")
	}
	if len(credentials) == 0 && len(choices) == 1 {
		return nil, 0, nil
	}

	sort.Sort(auth.ById(credentials))

	for {
		_, _ = fmt.Fprintln(os.Stderr)

		offset := 0
		for i, choice := range choices {
			_, _ = fmt.Fprintf(os.Stderr, "[%d]: %s\n", i+1, choice)
			offset++
		}

		if len(credentials) > 0 {
			_, _ = fmt.Fprintln(os.Stderr)
			_, _ = fmt.Fprintf(os.Stderr, "Existing %s for %s:\n", name, target)

			for i, cred := range credentials {
				meta := make([]string, 0, len(cred.Metadata()))
				for k, v := range cred.Metadata() {
					meta = append(meta, k+":"+v)
				}
				sort.Strings(meta)
				metaFmt := strings.Join(meta, ",")

				fmt.Printf("[%d]: %s => (%s) (%s)\n",
					i+1+offset,
					colors.Cyan(cred.ID().Human()),
					metaFmt,
					cred.CreateTime().Format(time.RFC822),
				)
			}
		}

		_, _ = fmt.Fprintln(os.Stderr)
		_, _ = fmt.Fprintf(os.Stderr, "Select option: ")

		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
		_, _ = fmt.Fprintln(os.Stderr)
		if err != nil {
			return nil, 0, err
		}

		line = strings.TrimSpace(line)
		index, err := strconv.Atoi(line)
		if err != nil || index < 1 || index > len(choices)+len(credentials) {
			_, _ = fmt.Fprintln(os.Stderr, "invalid input")
			continue
		}

		switch {
		case index <= len(choices):
			return nil, index - 1, nil
		default:
			return credentials[index-len(choices)-1], 0, nil
		}
	}
}