prompt.go

  1package input
  2
  3import (
  4	"bufio"
  5	"fmt"
  6	"net/url"
  7	"os"
  8	"sort"
  9	"strconv"
 10	"strings"
 11	"syscall"
 12	"time"
 13
 14	"golang.org/x/crypto/ssh/terminal"
 15
 16	"github.com/MichaelMure/git-bug/bridge/core/auth"
 17	"github.com/MichaelMure/git-bug/util/colors"
 18	"github.com/MichaelMure/git-bug/util/interrupt"
 19)
 20
 21// PromptValidator is a validator for a user entry
 22// If complaint is "", value is considered valid, otherwise it's the error reported to the user
 23// If err != nil, a terminal error happened
 24type PromptValidator func(name string, value string) (complaint string, err error)
 25
 26// Required is a validator preventing a "" value
 27func Required(name string, value string) (string, error) {
 28	if value == "" {
 29		return fmt.Sprintf("%s is empty", name), nil
 30	}
 31	return "", nil
 32}
 33
 34// IsURL is a validator checking that the value is a fully formed URL
 35func IsURL(name string, value string) (string, error) {
 36	u, err := url.Parse(value)
 37	if err != nil {
 38		return fmt.Sprintf("%s is invalid: %v", name, err), nil
 39	}
 40	if u.Scheme == "" {
 41		return fmt.Sprintf("%s is missing a scheme", name), nil
 42	}
 43	if u.Host == "" {
 44		return fmt.Sprintf("%s is missing a host", name), nil
 45	}
 46	return "", nil
 47}
 48
 49// Prompts
 50
 51// Prompt is a simple text input.
 52func Prompt(prompt, name string, validators ...PromptValidator) (string, error) {
 53	return PromptDefault(prompt, name, "", validators...)
 54}
 55
 56// PromptDefault is a simple text input with a default value.
 57func PromptDefault(prompt, name, preValue string, validators ...PromptValidator) (string, error) {
 58loop:
 59	for {
 60		if preValue != "" {
 61			_, _ = fmt.Fprintf(os.Stderr, "%s [%s]: ", prompt, preValue)
 62		} else {
 63			_, _ = fmt.Fprintf(os.Stderr, "%s: ", prompt)
 64		}
 65
 66		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
 67		if err != nil {
 68			return "", err
 69		}
 70
 71		line = strings.TrimSpace(line)
 72
 73		if preValue != "" && line == "" {
 74			line = preValue
 75		}
 76
 77		for _, validator := range validators {
 78			complaint, err := validator(name, line)
 79			if err != nil {
 80				return "", err
 81			}
 82			if complaint != "" {
 83				_, _ = fmt.Fprintln(os.Stderr, complaint)
 84				continue loop
 85			}
 86		}
 87
 88		return line, nil
 89	}
 90}
 91
 92// PromptPassword is a specialized text input that doesn't display the characters entered.
 93func PromptPassword(prompt, name string, validators ...PromptValidator) (string, error) {
 94	termState, err := terminal.GetState(syscall.Stdin)
 95	if err != nil {
 96		return "", err
 97	}
 98
 99	cancel := interrupt.RegisterCleaner(func() error {
100		return terminal.Restore(syscall.Stdin, termState)
101	})
102	defer cancel()
103
104loop:
105	for {
106		_, _ = fmt.Fprintf(os.Stderr, "%s: ", prompt)
107
108		bytePassword, err := terminal.ReadPassword(syscall.Stdin)
109		// new line for coherent formatting, ReadPassword clip the normal new line
110		// entered by the user
111		fmt.Println()
112
113		if err != nil {
114			return "", err
115		}
116
117		pass := string(bytePassword)
118
119		for _, validator := range validators {
120			complaint, err := validator(name, pass)
121			if err != nil {
122				return "", err
123			}
124			if complaint != "" {
125				_, _ = fmt.Fprintln(os.Stderr, complaint)
126				continue loop
127			}
128		}
129
130		return pass, nil
131	}
132}
133
134// PromptChoice is a prompt giving possible choices
135// Return the index starting at zero of the choice selected.
136func PromptChoice(prompt string, choices []string) (int, error) {
137	for {
138		for i, choice := range choices {
139			_, _ = fmt.Fprintf(os.Stderr, "[%d]: %s\n", i+1, choice)
140		}
141		_, _ = fmt.Fprintf(os.Stderr, "%s: ", prompt)
142
143		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
144		fmt.Println()
145		if err != nil {
146			return 0, err
147		}
148
149		line = strings.TrimSpace(line)
150
151		index, err := strconv.Atoi(line)
152		if err != nil || index < 1 || index > len(choices) {
153			_, _ = fmt.Fprintln(os.Stderr, "invalid input")
154			continue
155		}
156
157		return index - 1, nil
158	}
159}
160
161func PromptURLWithRemote(prompt, name string, validRemotes []string, validators ...PromptValidator) (string, error) {
162	if len(validRemotes) == 0 {
163		return Prompt(prompt, name, validators...)
164	}
165
166	sort.Strings(validRemotes)
167
168	for {
169		_, _ = fmt.Fprintln(os.Stderr, "\nDetected projects:")
170
171		for i, remote := range validRemotes {
172			_, _ = fmt.Fprintf(os.Stderr, "[%d]: %v\n", i+1, remote)
173		}
174
175		_, _ = fmt.Fprintf(os.Stderr, "\n[0]: Another project\n\n")
176		_, _ = fmt.Fprintf(os.Stderr, "Select option: ")
177
178		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
179		if err != nil {
180			return "", err
181		}
182
183		line = strings.TrimSpace(line)
184
185		index, err := strconv.Atoi(line)
186		if err != nil || index < 0 || index > len(validRemotes) {
187			_, _ = fmt.Fprintln(os.Stderr, "invalid input")
188			continue
189		}
190
191		// if user want to enter another project url break this loop
192		if index == 0 {
193			break
194		}
195
196		return validRemotes[index-1], nil
197	}
198
199	return Prompt(prompt, name, validators...)
200}
201
202func PromptCredential(target, name string, credentials []auth.Credential, choices []string) (auth.Credential, int, error) {
203	if len(credentials) == 0 && len(choices) == 0 {
204		return nil, 0, fmt.Errorf("no possible choice")
205	}
206	if len(credentials) == 0 && len(choices) == 1 {
207		return nil, 0, nil
208	}
209
210	sort.Sort(auth.ById(credentials))
211
212	for {
213		offset := 0
214		for i, choice := range choices {
215			_, _ = fmt.Fprintf(os.Stderr, "[%d]: %s\n", i+1, choice)
216			offset++
217		}
218
219		if len(credentials) > 0 {
220			_, _ = fmt.Fprintln(os.Stderr)
221			_, _ = fmt.Fprintf(os.Stderr, "Existing %s for %s:", name, target)
222
223			for i, cred := range credentials {
224				meta := make([]string, 0, len(cred.Metadata()))
225				for k, v := range cred.Metadata() {
226					meta = append(meta, k+":"+v)
227				}
228				sort.Strings(meta)
229				metaFmt := strings.Join(meta, ",")
230
231				fmt.Printf("[%d]: %s => (%s) (%s)\n",
232					i+1+offset,
233					colors.Cyan(cred.ID().Human()),
234					metaFmt,
235					cred.CreateTime().Format(time.RFC822),
236				)
237			}
238		}
239
240		_, _ = fmt.Fprintln(os.Stderr)
241		_, _ = fmt.Fprintf(os.Stderr, "Select option: ")
242
243		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
244		_, _ = fmt.Fprintln(os.Stderr)
245		if err != nil {
246			return nil, 0, err
247		}
248
249		line = strings.TrimSpace(line)
250		index, err := strconv.Atoi(line)
251		if err != nil || index < 1 || index > len(choices)+len(credentials) {
252			_, _ = fmt.Fprintln(os.Stderr, "invalid input")
253			continue
254		}
255
256		switch {
257		case index <= len(choices):
258			return nil, index - 1, nil
259		default:
260			return credentials[index-len(choices)-1], 0, nil
261		}
262	}
263}