/
goline.go
447 lines (415 loc) · 10.3 KB
/
goline.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
// Copyright 2011, Bryan Matsuo. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
* Filename: goline.go
* Package: goline
* Author: Bryan Matsuo <bmatsuo@soe.ucsc.edu>
* Created: Sat Aug 13 02:28:54 PDT 2011
* Description:
*/
/*
Package goline is a command line interfacing (prompting) library inspired
by Ruby's HighLine.
Differences for HighLine users:
- To be more Go-ish, where HighLine uses the term "strip", the package
uses "trim".
- Instead of an `Agree(question, config) bool` function, the package
provides a function `Confirm(question, yesorno, config) bool`. This is
because the author things the term "agree" implies the desire of a
positive response to the question ("yes").
See github.com/bmatsuo/goline/examples for examples using goline.
*/
package goline
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"reflect"
"strings"
"unicode"
"unicode/utf8"
)
func fSay(wr io.Writer, msg string, trim bool) (int, error) {
if trim {
msg = strings.TrimRightFunc(msg, unicode.IsSpace)
}
if c, _ := utf8.DecodeLastRuneInString(msg); unicode.IsSpace(c) {
return fmt.Fprint(wr, msg)
}
return fmt.Fprintln(wr, msg)
}
// A simple function for printing (single-line) messages and prompts to
// os.Stdout. If trailing whitespace is present in the given message, it
// will be printed as given. Otherwise, a trailing newline '\n' will be
// printed after the message.
// goline.Say("Hello, World!") // Prints "Hello, World!\n"
// goline.Say("Hello, World! ") // Prints "Hello, World! "
// See also, SayTrimmed.
func Say(msg string) (int, error) {
if c, _ := utf8.DecodeLastRuneInString(msg); unicode.IsSpace(c) {
return fmt.Print(msg)
}
return fmt.Println(msg)
}
// Like Say, but trailing whitespace is removed from the message before
// an internal call to Say is made.
// goline.SayTrimmed("Hello, World! \n\t\t") // Prints "Hello, World!\n"
func SayTrimmed(msg string) (int, error) {
return Say(strings.TrimRightFunc(msg, unicode.IsSpace))
}
type ListMode uint
const (
ColumnsAcross ListMode = iota
ColumnsDown
Inline
Rows
)
// Print a list of items to os.Stdout. The list can be formatted into rows
// or into a matrix using the ListMode argument. The third argument has
// different meaning (and type) depending on the mode.
// MODE OPTION DEFAULT MEANING
// Rows n/a
// Inline string " or " Join terminal element (e.g. "a, b, or c")
// Columns* int 80 Maximum line width
// If the default option is desired, it should be passed as nil.
// goline.List([]string{"cat", "dog", "go fish"}, goline.ColumnsAcross, nil)
// /* Outputs:
// * cat dog go fish
// */
// goline.List([]string{"cat", "dog", "go fish"}, goline.ColumnsDown, 15)
// /* Outputs:
// * cat go fish
// * dog
// */
// goline.List([]string{"cat", "dog", "go fish"}, goline.Inline, " and ")
// /* Outputs:
// * cat, dog, and go fish
// */
// goline.List([]string{"cat", "dog", "go fish"}, goline.Rows, nil)
// /* Outputs:
// * cat
// * dog
// * go fish
// */
// See subdirectory examples/goline-lists.
func List(items interface{}, mode ListMode, option interface{}) {
ival := reflect.ValueOf(items)
itype := ival.Type()
if k := itype.Kind(); k != reflect.Slice {
panic(errors.New("List given non-Slice types."))
}
strs := make([]string, ival.Len())
for i := range strs {
v := ival.Index(i).Interface()
switch v.(type) {
case Stringer:
strs[i] = v.(Stringer).String()
case string:
strs[i] = v.(string)
default:
panic(errors.New("List items contain non-string, non-Stringer item"))
}
}
switch mode {
case ColumnsAcross:
fallthrough
case ColumnsDown:
wrap := 80
switch option.(type) {
case nil:
case int:
wrap = option.(int)
default:
panic(errors.New("List option of unacceptable type"))
}
var width int
for i := range strs {
if n := len(strs[i]); n > width {
width = n
}
}
n := len(strs)
ncols := (wrap + 1) / (width + 1)
if ncols <= 1 {
// Just print rows if no more than 1 column fits.
for i := range strs {
SayTrimmed(strs[i])
}
break
}
nrows := (n + ncols - 1) / ncols
sfmt := fmt.Sprintf("%%-%ds", width)
for i := range strs {
strs[i] = fmt.Sprintf(sfmt, strs[i])
}
switch mode {
case ColumnsAcross:
for i := 0; i < n; i += ncols {
end := i + ncols
if end > n {
end = n
}
row := strs[i:end]
SayTrimmed(strings.Join(row, " "))
}
case ColumnsDown:
for i := 0; i < nrows; i++ {
var row []string
for j := 0; j < ncols; j++ {
index := j*nrows + i
if index >= n {
break
}
row = append(row, strs[index])
}
SayTrimmed(strings.Join(row, " "))
}
}
case Inline:
n := len(strs)
if n == 1 {
SayTrimmed(strs[0])
break
}
join := "or "
switch option.(type) {
case nil:
case string:
join = option.(string)
default:
panic(errors.New("List option of unacceptable type"))
}
if n == 2 {
Say(strings.Join([]string{strs[n-2], join, strs[n-2], "\n"}, ""))
break
}
strs[n-1] = join + strs[n-1]
SayTrimmed(strings.Join(strs, ", "))
case Rows:
for i := range strs {
SayTrimmed(strs[i])
}
default:
panic(errors.New("Unknown mode"))
}
}
// Prompt the user for text input. The result is stored in dest, which must
// be a pointer to a native Go type (int, uint16, string, float32, ...).
// Slice types are not currently supported. List input must be done with a
// *string destination and post-processing.
// package main
// import (
// "goline"
// "os"
// )
// func main() {
// timeout := 5e3
// goline.Ask(&timeout, "Timeout (ms)? ", func(q *goline.Question) {
// q.Default = timeout
// q.In(goline.IntBoundedStrictly(goline.Above, 0))
// q.Panic = func(e os.Error) { panic(e) }
// })
// }
func Ask(dest interface{}, msg string, config func(*Question)) (e error) {
var q *Question
defer func() {
if err := recover(); err != nil {
switch err.(type) {
case error:
// Call a panic method...
if q.Panic != nil {
q.Panic(err.(error))
}
default:
panic(err)
}
}
}()
if k := reflect.TypeOf(dest).Kind(); k != reflect.Ptr && k != reflect.Slice {
panicUnrecoverable(fmt.Errorf("Ask(...) requires a Ptr type, not %s", k.String()))
return
} else if k == reflect.Slice {
panicUnrecoverable(fmt.Errorf("Ask(...) can not currently assign to slices."))
return
}
var t Type
switch dest.(type) {
case *uint:
t = Uint
case *uint8:
t = Uint
case *uint16:
t = Uint
case *uint32:
t = Uint
case *uint64:
t = Uint
case *int:
t = Int
case *int8:
t = Int
case *int16:
t = Int
case *int32:
t = Int
case *int64:
t = Int
case *float32:
t = Float
case *float64:
t = Float
case *string:
t = String
default:
fmt.Errorf("Unusable destination")
}
q = newQuestion(t)
q.Question = msg
if config != nil {
config(q)
}
if err := q.tryFirstAnswer(); err == nil && q.val != nil {
if err := q.setDest(dest); err != nil {
panicUnrecoverable(err)
q.val = nil
}
return
}
prompt := msg
contFunc := func(err error) {
Say(fmt.Sprintf("Error: %s\n", err.Error()))
prompt = q.Responses[AskOnError]
}
r := bufio.NewReader(os.Stdin)
for {
tail := stringSuffixFunc(prompt, unicode.IsSpace)
Say(prompt + q.defaultString(tail))
var resp []byte
for cont := true; cont; {
s, isPrefix, err := r.ReadLine()
if err != nil {
panicUnrecoverable(err)
return
}
resp = append(resp, s...)
cont = isPrefix
}
if err := q.parse(string(resp)); err != nil {
panicUnrecoverable(err)
contFunc(err)
continue
}
// Cast the result from a wide (e.g. 64bit) type to the desired type.
// This should not fail under any normal circumstances, so failure
// should break the loop.
if err := q.setDest(dest); err != nil {
panicUnrecoverable(err)
contFunc(err)
continue
}
break
}
return
}
// Corresponds to HighLine's `agree` method. A simple wrapper around Ask for
// yes/no questions. Confirm is given a string to prompt the user with, and a
// default (or expected) value (yes=true, no=false). Returns the value of the
// input.
// if Confirm("Fetch data from the server? ", true, nil) {
// var server string
// Ask(&server, "Server (host:port)? ", nil)
// // Fetch some data...
// }
func Confirm(question string, yes bool, config func(*Question)) bool {
def := "no"
if yes {
def = "yes"
}
var okstr string
var err error
Ask(&okstr, question, func(q *Question) {
q.Default = def
q.In(StringSet{"yes", "y", "no", "n"})
if config != nil {
config(q)
}
if q.Panic != nil {
f := q.Panic
q.Panic = func(e error) {
err = e
f(e)
}
}
})
if err != nil {
return false
}
if okstr[0] == 'y' {
return true
}
return false
}
func splitShellCmd(cmd string) (name, args string) {
cmd = strings.TrimLeftFunc(cmd, unicode.IsSpace)
switch pre := strings.IndexFunc(cmd, unicode.IsSpace); {
case pre > 0:
name, args = cmd[:pre], strings.TrimLeftFunc(cmd[pre:], unicode.IsSpace)
case pre == -1:
name = cmd
default:
panic("unexpected case (untrimmed)")
}
return
}
// Prompt the user to choose from a list of choices. Return the index
// of the chosen item, and the item itself in an empty interface. See
// Menu for more information about configuring the prompt.
func Choose(config func(*Menu)) (i int, v interface{}) {
i = -1
m := newMenu()
config(m)
if m.Len() == 0 {
if m.Panic != nil {
m.Panic(ErrorNoChoices)
return
}
panic(ErrorNoChoices)
}
if len(m.Header) > 0 {
Say(m.Header)
}
raw, selections, tr := m.Selections()
List(raw, m.ListMode, nil)
ok := true
var resp, args string
Ask(&resp, m.Question, func(q *Question) {
var set AnswerSet = StringSet(selections)
if m.Shell {
set = shellCommandSet(StringCompletionSet(set.(StringSet)))
}
q.In(set)
q.Panic = func(err error) {
ok = false
if m.Panic != nil {
m.Panic(err)
} else {
panic(err)
}
}
})
if !ok {
return
}
if m.Shell {
resp, args = splitShellCmd(resp)
}
i = tr[resp]
v = m.Choices[i]
if m.Actions[i] != nil {
m.Actions[i](resp, args)
}
return
}