prompt.go

v0.6.1
Doc Versions Source
1
package report
2
3
import (
4
	"bufio"
5
	"context"
6
	"fmt"
7
	"io"
8
	"strconv"
9
	"strings"
10
11
	"go.bigb.es/auxilia/culpa"
12
	"go.bigb.es/auxilia/legatus"
13
)
14
15
var (
16
	_ legatus.Prompter = (*CLIPrompter)(nil)
17
	_ legatus.Prompter = AutoPrompter(false)
18
)
19
20
// CLIPrompter answers interactive questions by reading from In and writing
21
// prompts to Out. It never touches os.Stdin/os.Stdout directly (PC2).
22
type CLIPrompter struct {
23
	In  io.Reader
24
	Out io.Writer
25
26
	reader *bufio.Reader
27
}
28
29
// readLine lazily initializes a buffered reader and returns the next trimmed
30
// line of input.
31
func (p *CLIPrompter) readLine() (string, error) {
32
	if p.reader == nil {
33
		p.reader = bufio.NewReader(p.In)
34
	}
35
	line, err := p.reader.ReadString('\n')
36
	if err != nil && line == "" {
37
		return "", err
38
	}
39
	return strings.TrimSpace(line), nil
40
}
41
42
// Confirm asks a yes/no question. An empty or unrecognized answer is treated as
43
// "no". A read error before any input is returned.
44
func (p *CLIPrompter) Confirm(_ context.Context, question string) (bool, error) {
45
	fmt.Fprintf(p.Out, "%s [y/N] ", question)
46
	line, err := p.readLine()
47
	if err != nil {
48
		return false, err
49
	}
50
	switch strings.ToLower(line) {
51
	case "y", "yes":
52
		return true, nil
53
	default:
54
		return false, nil
55
	}
56
}
57
58
// Select presents numbered options and reads a 1-based choice, returning the
59
// 0-based index. A non-numeric or out-of-range answer is an error.
60
func (p *CLIPrompter) Select(_ context.Context, question string, options []string) (int, error) {
61
	fmt.Fprintf(p.Out, "%s\n", question)
62
	for i, opt := range options {
63
		fmt.Fprintf(p.Out, "  %d) %s\n", i+1, opt)
64
	}
65
	fmt.Fprintf(p.Out, "select [1-%d]: ", len(options))
66
	line, err := p.readLine()
67
	if err != nil {
68
		return -1, err
69
	}
70
	n, err := strconv.Atoi(line)
71
	if err != nil {
72
		return -1, culpa.WithCode(culpa.Wrapf(err, "report: invalid selection %q", line), "LEGATUS_BAD_SELECTION")
73
	}
74
	if n < 1 || n > len(options) {
75
		return -1, culpa.WithCode(culpa.New(fmt.Sprintf("report: selection %d out of range [1-%d]", n, len(options))), "LEGATUS_BAD_SELECTION")
76
	}
77
	return n - 1, nil
78
}
79
80
// AutoPrompter is a non-interactive prompter that answers every Confirm with a
81
// fixed boolean and every Select with the first option.
82
type AutoPrompter bool
83
84
// Confirm returns the configured fixed answer.
85
func (a AutoPrompter) Confirm(context.Context, string) (bool, error) {
86
	return bool(a), nil
87
}
88
89
// Select always picks the first option.
90
func (AutoPrompter) Select(_ context.Context, _ string, options []string) (int, error) {
91
	if len(options) == 0 {
92
		return -1, culpa.WithCode(culpa.New("report: select with no options"), "LEGATUS_BAD_SELECTION")
93
	}
94
	return 0, nil
95
}
96

Source Files