aboutsummaryrefslogtreecommitdiff
path: root/pkg/cli/interface.go
blob: 4d0e19690254bff3463e38e53f083c48a46e1e38 (plain)
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
/*******************************************************************************
*
* Copyright 2017 Stefan Majewsky <majewsky@gmx.net>
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*
*******************************************************************************/

package cli

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"os"
	"sort"
	"strings"

	"golang.org/x/crypto/ssh/terminal"
)

//Interface wraps access to the CLI, including input, output and subprocesses.
var Interface *Implementation

func init() {
	SetupInterface(os.Stdin, os.Stdout, os.Stderr, DefaultCommandRunner)
}

//SetupInterface prepares the Interface instance with nonstandard file streams
//or a nonstandard CommandRunner. This is only required for unit tests.
func SetupInterface(stdin io.Reader, stdout, stderr io.Writer, commandRunner CommandRunner) {
	Interface = &Implementation{
		stdin:         stdin,
		stdout:        stdout,
		stderr:        stderr,
		stdinBuf:      bufio.NewReader(stdin),
		commandRunner: commandRunner,
	}

	if stdinFile, ok := stdin.(*os.File); ok && terminal.IsTerminal(int(stdinFile.Fd())) {
		Interface.tui = &terminalTUI{Interface}
	} else {
		Interface.tui = &pipeTUI{Interface}
	}
}

//Implementation wraps access to the CLI, including input, output and subprocesses.
type Implementation struct {
	stdin         io.Reader
	stdout        io.Writer
	stderr        io.Writer
	stdinBuf      *bufio.Reader
	tui           TUI
	commandRunner CommandRunner
	//If this flag is set, only ShowResult() will write into stdout; everything
	//else that usually goes to stdout goes to stderr instead.
	//
	//This is useful when gofu is expected to output a certain value to stdout
	//which is used by the next program in the pipe, and additional output from
	//subprocesses could confuse the stdout handler.
	StdoutProtected bool
}

//TUI provides the interactive parts of the cli.Implementation, so that these can be
//easily swapped out for mock implementations in unit tests.
type TUI interface {
	//ReadLine reads a line from stdin (if tty: uses canonical mode).
	ReadLine(prompt string) (string, error)
	//Confirm displays a yes/no question and returns whether the user answered "yes".
	Confirm(question string) (bool, error)
	//Query displays a question and a set of answers and allows the user to select
	//one of the answers. Returns the Return attribute of the selected Choice.
	Query(prompt string, choices ...Choice) (string, error)
	//Print writes the given string (potentially including ANSI escape codes) to
	//the given writer. At this point, it can be decided whether to strip out the
	//ANSI escape codes.
	Print(w io.Writer, msg string)
}

func (i *Implementation) safeStdout() io.Writer {
	if i.StdoutProtected {
		return i.stderr
	}
	return i.stdout
}

////////////////////////////////////////////////////////////////////////////////
// input

//ReadLine reads a line from stdin (if tty: uses canonical mode).
func (i *Implementation) ReadLine(prompt string) (string, error) {
	return i.tui.ReadLine(prompt)
}

//Confirm displays a yes/no question and returns whether the user answered "yes".
func (i *Implementation) Confirm(question string) (bool, error) {
	return i.tui.Confirm(question)
}

//Query displays a question and a set of answers and allows the user to select
//one of the answers. Returns the Return attribute of the selected Choice.
func (i *Implementation) Query(prompt string, choices ...Choice) (string, error) {
	return i.tui.Query(prompt, choices...)
}

////////////////////////////////////////////////////////////////////////////////
// subprocesses

//Run executes the given command on the same stdout and stderr.
func (i *Implementation) Run(c Command) error {
	return i.commandRunner(c, nil, i.safeStdout(), i.stderr)
}

//CaptureStdout executes the given command on the same stderr and captures its stdout.
func (i *Implementation) CaptureStdout(c Command) (string, error) {
	var buf bytes.Buffer
	err := i.commandRunner(c, nil, &buf, i.stderr)
	return string(buf.Bytes()), err
}

////////////////////////////////////////////////////////////////////////////////
// output

//ShowResult displays the result of a computation on stdout.
func (i *Implementation) ShowResult(str string) {
	str = strings.TrimSpace(str) + "\n"
	i.stdout.Write([]byte(str))
}

//ShowResultsSorted calls ShowResult() on each of the results after sorting them.
func (i *Implementation) ShowResultsSorted(strs []string) {
	sort.Strings(strs)
	for _, str := range strs {
		i.ShowResult(str)
	}
}

//ShowProgress displays a progress message on stderr.
func (i *Implementation) ShowProgress(str string) {
	i.tui.Print(i.stderr, fmt.Sprintf("\x1B[0;1;36m>>\x1B[0;36m %s\x1B[0m", strings.TrimSpace(str)))
}

//ShowWarning displays a warning message on stderr.
func (i *Implementation) ShowWarning(str string) {
	i.tui.Print(i.stderr, fmt.Sprintf("\x1B[0;1;33m!!\x1B[0;36m %s\x1B[0m", strings.TrimSpace(str)))
}

//ShowError displays an error message on stderr.
func (i *Implementation) ShowError(str string) {
	i.tui.Print(i.stderr, fmt.Sprintf("\x1B[0;1;31m!!\x1B[0;36m %s\x1B[0m", strings.TrimSpace(str)))
}

//ShowUsage displays a usage synopsis on stderr.
func (i *Implementation) ShowUsage(str string) {
	str = strings.TrimSpace(str) + "\n"
	i.stderr.Write([]byte(str))
}