prompt.go

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