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
 51func Prompt(prompt, name string, validators ...PromptValidator) (string, error) {
 52	return PromptDefault(prompt, name, "", validators...)
 53}
 54
 55func PromptDefault(prompt, name, preValue string, validators ...PromptValidator) (string, error) {
 56loop:
 57	for {
 58		if preValue != "" {
 59			_, _ = fmt.Fprintf(os.Stderr, "%s [%s]: ", prompt, preValue)
 60		} else {
 61			_, _ = fmt.Fprintf(os.Stderr, "%s: ", prompt)
 62		}
 63
 64		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
 65		if err != nil {
 66			return "", err
 67		}
 68
 69		line = strings.TrimSpace(line)
 70
 71		if preValue != "" && line == "" {
 72			line = preValue
 73		}
 74
 75		for _, validator := range validators {
 76			complaint, err := validator(name, line)
 77			if err != nil {
 78				return "", err
 79			}
 80			if complaint != "" {
 81				_, _ = fmt.Fprintln(os.Stderr, complaint)
 82				continue loop
 83			}
 84		}
 85
 86		return line, nil
 87	}
 88}
 89
 90func PromptPassword(prompt, name string, validators ...PromptValidator) (string, error) {
 91	termState, err := terminal.GetState(syscall.Stdin)
 92	if err != nil {
 93		return "", err
 94	}
 95
 96	cancel := interrupt.RegisterCleaner(func() error {
 97		return terminal.Restore(syscall.Stdin, termState)
 98	})
 99	defer cancel()
100
101loop:
102	for {
103		_, _ = fmt.Fprintf(os.Stderr, "%s: ", prompt)
104
105		bytePassword, err := terminal.ReadPassword(syscall.Stdin)
106		// new line for coherent formatting, ReadPassword clip the normal new line
107		// entered by the user
108		fmt.Println()
109
110		if err != nil {
111			return "", err
112		}
113
114		pass := string(bytePassword)
115
116		for _, validator := range validators {
117			complaint, err := validator(name, pass)
118			if err != nil {
119				return "", err
120			}
121			if complaint != "" {
122				_, _ = fmt.Fprintln(os.Stderr, complaint)
123				continue loop
124			}
125		}
126
127		return pass, nil
128	}
129}
130
131func PromptChoice(prompt string, choices []string) (int, error) {
132	for {
133		for i, choice := range choices {
134			_, _ = fmt.Fprintf(os.Stderr, "[%d]: %s\n", i+1, choice)
135		}
136		_, _ = fmt.Fprintf(os.Stderr, "%s: ", prompt)
137
138		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
139		fmt.Println()
140		if err != nil {
141			return 0, err
142		}
143
144		line = strings.TrimSpace(line)
145
146		index, err := strconv.Atoi(line)
147		if err != nil || index < 1 || index > len(choices) {
148			_, _ = fmt.Fprintf(os.Stderr, "invalid input")
149			continue
150		}
151
152		return index - 1, nil
153	}
154}
155
156func PromptURLWithRemote(prompt, name string, validRemotes []string, validators ...PromptValidator) (string, error) {
157	if len(validRemotes) == 0 {
158		return Prompt(prompt, name, validators...)
159	}
160
161	sort.Strings(validRemotes)
162
163	for {
164		_, _ = fmt.Fprintln(os.Stderr, "\nDetected projects:")
165
166		for i, remote := range validRemotes {
167			_, _ = fmt.Fprintf(os.Stderr, "[%d]: %v\n", i+1, remote)
168		}
169
170		_, _ = fmt.Fprintf(os.Stderr, "\n[0]: Another project\n\n")
171		_, _ = fmt.Fprintf(os.Stderr, "Select option: ")
172
173		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
174		if err != nil {
175			return "", err
176		}
177
178		line = strings.TrimSpace(line)
179
180		index, err := strconv.Atoi(line)
181		if err != nil || index < 0 || index > len(validRemotes) {
182			_, _ = fmt.Fprintf(os.Stderr, "invalid input")
183			continue
184		}
185
186		// if user want to enter another project url break this loop
187		if index == 0 {
188			break
189		}
190
191		return validRemotes[index-1], nil
192	}
193
194	return Prompt(prompt, name, validators...)
195}
196
197func PromptCredential(target, name string, credentials []auth.Credential, choices []string) (auth.Credential, int, error) {
198	if len(credentials) == 0 && len(choices) == 0 {
199		return nil, 0, fmt.Errorf("no possible choice")
200	}
201	if len(credentials) == 0 && len(choices) == 1 {
202		return nil, 0, nil
203	}
204
205	sort.Sort(auth.ById(credentials))
206
207	for {
208		offset := 0
209		for i, choice := range choices {
210			_, _ = fmt.Fprintf(os.Stderr, "[%d]: %s\n", i+1, choice)
211			offset++
212		}
213
214		if len(credentials) > 0 {
215			_, _ = fmt.Fprintln(os.Stderr)
216			_, _ = fmt.Fprintf(os.Stderr, "Existing %s for %s:", name, target)
217
218			for i, cred := range credentials {
219				meta := make([]string, 0, len(cred.Metadata()))
220				for k, v := range cred.Metadata() {
221					meta = append(meta, k+":"+v)
222				}
223				sort.Strings(meta)
224				metaFmt := strings.Join(meta, ",")
225
226				fmt.Printf("[%d]: %s => (%s) (%s)\n",
227					i+1+offset,
228					colors.Cyan(cred.ID().Human()),
229					metaFmt,
230					cred.CreateTime().Format(time.RFC822),
231				)
232			}
233		}
234
235		_, _ = fmt.Fprintln(os.Stderr)
236		_, _ = fmt.Fprintf(os.Stderr, "Select option: ")
237
238		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
239		_, _ = fmt.Fprintln(os.Stderr)
240		if err != nil {
241			return nil, 0, err
242		}
243
244		line = strings.TrimSpace(line)
245		index, err := strconv.Atoi(line)
246		if err != nil || index < 1 || index > len(choices)+len(credentials) {
247			_, _ = fmt.Fprintln(os.Stderr, "invalid input")
248			continue
249		}
250
251		switch {
252		case index < len(choices):
253			return nil, index - 1, nil
254		default:
255			return credentials[index-len(choices)-1], 0, nil
256		}
257	}
258}