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}