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(int(syscall.Stdin))
95 if err != nil {
96 return "", err
97 }
98
99 cancel := interrupt.RegisterCleaner(func() error {
100 return terminal.Restore(int(syscall.Stdin), termState)
101 })
102 defer cancel()
103
104loop:
105 for {
106 _, _ = fmt.Fprintf(os.Stderr, "%s: ", prompt)
107
108 bytePassword, err := terminal.ReadPassword(int(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 _, _ = fmt.Fprintln(os.Stderr)
214
215 offset := 0
216 for i, choice := range choices {
217 _, _ = fmt.Fprintf(os.Stderr, "[%d]: %s\n", i+1, choice)
218 offset++
219 }
220
221 if len(credentials) > 0 {
222 _, _ = fmt.Fprintln(os.Stderr)
223 _, _ = fmt.Fprintf(os.Stderr, "Existing %s for %s:\n", name, target)
224
225 for i, cred := range credentials {
226 meta := make([]string, 0, len(cred.Metadata()))
227 for k, v := range cred.Metadata() {
228 meta = append(meta, k+":"+v)
229 }
230 sort.Strings(meta)
231 metaFmt := strings.Join(meta, ",")
232
233 fmt.Printf("[%d]: %s => (%s) (%s)\n",
234 i+1+offset,
235 colors.Cyan(cred.ID().Human()),
236 metaFmt,
237 cred.CreateTime().Format(time.RFC822),
238 )
239 }
240 }
241
242 _, _ = fmt.Fprintln(os.Stderr)
243 _, _ = fmt.Fprintf(os.Stderr, "Select option: ")
244
245 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
246 _, _ = fmt.Fprintln(os.Stderr)
247 if err != nil {
248 return nil, 0, err
249 }
250
251 line = strings.TrimSpace(line)
252 index, err := strconv.Atoi(line)
253 if err != nil || index < 1 || index > len(choices)+len(credentials) {
254 _, _ = fmt.Fprintln(os.Stderr, "invalid input")
255 continue
256 }
257
258 switch {
259 case index <= len(choices):
260 return nil, index - 1, nil
261 default:
262 return credentials[index-len(choices)-1], 0, nil
263 }
264 }
265}