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}