summaryrefslogtreecommitdiff
path: root/pkg/cli/query.go
diff options
context:
space:
mode:
authorStefan Majewsky <majewsky@gmx.net>2017-05-04 22:36:53 +0200
committerStefan Majewsky <majewsky@gmx.net>2017-05-04 22:36:53 +0200
commitec4270c43f1ddd37d1eb19da5db78129b93f72b7 (patch)
tree67ab3eab1e71ef1b5479f3f5a00e040b622d7191 /pkg/cli/query.go
parentc69e9f604c47469429a6a8a27ba7ef873134e0a1 (diff)
downloadgofu-ec4270c43f1ddd37d1eb19da5db78129b93f72b7.tar.gz
add "cli" package with a Query method
Diffstat (limited to 'pkg/cli/query.go')
-rw-r--r--pkg/cli/query.go186
1 files changed, 186 insertions, 0 deletions
diff --git a/pkg/cli/query.go b/pkg/cli/query.go
new file mode 100644
index 0000000..a81d153
--- /dev/null
+++ b/pkg/cli/query.go
@@ -0,0 +1,186 @@
+/*******************************************************************************
+*
+* 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 (
+ "fmt"
+ "io"
+ "os"
+ "regexp"
+ "strings"
+
+ terminal "golang.org/x/crypto/ssh/terminal"
+)
+
+//Choice is a thing that the user can choose during Query().
+type Choice struct {
+ //If given, the choice can be selected by pressing the key that
+ //produces this character.
+ Shortcut byte
+ //The display string that describes this choice.
+ Text string
+}
+
+func (c Choice) hasShortcut() bool {
+ return c.Shortcut != '\000'
+}
+
+//Query displays a question and a set of answers and allows the user to select
+//one of the answers. Returns the selected Choice instance, as well as its
+//index in the original choices list (starting from 0).
+func Query(prompt string, choices ...Choice) (Choice, int) {
+ if len(choices) == 0 {
+ return Choice{}, -1
+ }
+
+ //disable line wrap during doQuery(); unexpected wrapping would confuse our
+ //cursor-moving code
+ os.Stdout.Write([]byte("\x1B[?7l"))
+ defer os.Stdout.Write([]byte("\x1B[?7h"))
+
+ //display question
+ os.Stdout.Write([]byte(strings.TrimSuffix(prompt, "\n") + "\n"))
+ selected := 0
+
+ buf := buffer{Input: os.Stdin}
+OUTER:
+ for {
+ displayChoices(os.Stdout, choices, selected)
+
+ input := buf.getNextInput()
+ if len(input) == 1 {
+ //single character entered - check if a shortcut matches
+ for idx, choice := range choices {
+ if choice.Shortcut == input[0] {
+ selected = idx
+ break OUTER
+ }
+ }
+ }
+
+ switch string(input) {
+ case "\r", "\n":
+ break OUTER
+ case "\x1B[A": // Up arrow key
+ selected--
+ if selected < 0 {
+ selected = 0
+ }
+ case "\x1B[B": // Down arrow key
+ selected++
+ if selected >= len(choices) {
+ selected = len(choices) - 1
+ }
+ case "\x03": // Ctrl-C
+ fmt.Fprintf(os.Stderr, "Interrupted!")
+ os.Exit(255)
+ default:
+ fmt.Printf("%#v\n", string(input))
+ }
+
+ //prepare to re-render choices
+ removeDisplayLines(len(choices))
+ }
+
+ //clear query display
+ removeDisplayLines(len(choices) + 1)
+
+ //display question + chosen answer
+ fmt.Fprintf(os.Stdout, "%s -> %s\n",
+ strings.TrimSuffix(prompt, "\n"),
+ strings.TrimSpace(choices[selected].Text),
+ )
+
+ return choices[selected], selected
+}
+
+func removeDisplayLines(n int) {
+ for idx := 0; idx < n; idx++ {
+ os.Stdout.Write([]byte("\x1B[A\x1B[2K"))
+ }
+}
+
+func displayChoices(out io.Writer, choices []Choice, selectedIndex int) {
+ hasShortcuts := false
+ for _, choice := range choices {
+ if choice.hasShortcut() {
+ hasShortcuts = true
+ break
+ }
+ }
+
+ for idx, choice := range choices {
+ text := " " + strings.TrimSpace(choice.Text) + " \n"
+ if hasShortcuts {
+ shortcut := choice.Shortcut
+ if !choice.hasShortcut() {
+ shortcut = ' '
+ }
+ text = fmt.Sprintf(" [%c]%s", shortcut, text)
+ }
+
+ if idx == selectedIndex {
+ out.Write(Styled(text, AnsiInverse).DisplayString(true))
+ } else {
+ out.Write([]byte(text))
+ }
+ }
+}
+
+var ansiEscapeRx = regexp.MustCompile(`^\x1B\[[\x20-\x3F]*[\x40-\x7E]`)
+
+type buffer struct {
+ Input io.Reader
+ buf [128]byte
+ fill int
+}
+
+func (b *buffer) getNextInput() []byte {
+ //do we have a simple input character?
+ if b.fill > 0 && b.buf[0] != '\x1B' {
+ result := append([]byte(nil), b.buf[0])
+ copy(b.buf[0:], b.buf[1:])
+ b.fill--
+ return result
+ }
+
+ //do we have a full ANSI escape sequence?
+ match := ansiEscapeRx.Find(b.buf[0:b.fill])
+ if match != nil {
+ result := append([]byte(nil), match...)
+ copy(b.buf[0:], b.buf[len(match):])
+ b.fill -= len(match)
+ return result
+ }
+
+ oldState, err := terminal.MakeRaw(0)
+ if err != nil {
+ panic(err)
+ }
+ defer terminal.Restore(0, oldState)
+
+ //fill buffer some more
+ n, err := os.Stdin.Read(b.buf[b.fill:])
+ if err != nil {
+ panic(err)
+ }
+ b.fill += n
+
+ return b.getNextInput() //restart
+}