diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2020-03-28 13:53:10 +0100 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2020-03-28 13:53:10 +0100 |
| commit | ed2b9374599cc00467b1c27121318748cc0d3f49 (patch) | |
| tree | 65e3200a0b69817b24ae772223d1d852a52458af /internal/cli/query.go | |
| parent | 1ff86053ddef8de3247909434e0bd7000c060f2a (diff) | |
| download | gofu-ed2b9374599cc00467b1c27121318748cc0d3f49.tar.gz | |
rename pkg/ to internal/
Diffstat (limited to 'internal/cli/query.go')
| -rw-r--r-- | internal/cli/query.go | 295 |
1 files changed, 295 insertions, 0 deletions
diff --git a/internal/cli/query.go b/internal/cli/query.go new file mode 100644 index 0000000..add170c --- /dev/null +++ b/internal/cli/query.go @@ -0,0 +1,295 @@ +/******************************************************************************* +* +* 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 ( + "errors" + "fmt" + "io" + "regexp" + "strconv" + "strings" + + terminal "golang.org/x/crypto/ssh/terminal" +) + +//cannot use `var errInterrupted = errors.New("Interrupted!")` because golint +//complains about the formatting of the error message +type errInterrupted struct{} + +func (e errInterrupted) Error() string { + return "Interrupted!" +} + +//////////////////////////////////////////////////////////////////////////////// +// TUI implementation for when stdin is a terminal + +type terminalTUI struct { + i *Implementation +} + +func (t terminalTUI) Print(w io.Writer, msg string) { + w.Write([]byte(msg)) +} + +func (t terminalTUI) ReadLine(prompt string) (string, error) { + if prompt != "" { + t.i.safeStdout().Write([]byte(strings.TrimSpace(prompt) + " ")) + } + str, err := t.i.stdinBuf.ReadString('\n') + return strings.TrimSpace(str), err +} + +func (t terminalTUI) Confirm(question string) (bool, error) { + out := t.i.safeStdout() + out.Write([]byte(strings.TrimSpace(question) + " [y/n] ")) + + buf := buffer{Input: t.i.stdin} + for { + switch string(buf.getNextInput()) { + case "y", "Y": + out.Write([]byte("-> yes\n")) + return true, nil + case "n", "N": + out.Write([]byte("-> no\n")) + return false, nil + case "\x03": // Ctrl-C + return false, errInterrupted{} + } + } +} + +//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 + //The string to return from Implementation.Query(). + Return string +} + +func (c Choice) hasShortcut() bool { + return c.Shortcut != '\000' +} + +func (t terminalTUI) Query(prompt string, choices ...Choice) (string, error) { + if len(choices) == 0 { + panic("no choices") + } + + //disable line wrap; unexpected wrapping would confuse our cursor-moving code + out := t.i.safeStdout() + out.Write([]byte("\x1B[?7l")) + defer out.Write([]byte("\x1B[?7h")) + + //display question + out.Write([]byte(strings.TrimSuffix(prompt, "\n") + "\n")) + selected := 0 + + buf := buffer{Input: t.i.stdin} +OUTER: + for { + displayChoices(out, 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 + return "", errInterrupted{} + } + + //prepare to re-render choices + removeDisplayLines(out, len(choices)) + } + + //clear query display + removeDisplayLines(out, len(choices)+1) + + //display question + chosen answer + fmt.Fprintf(out, "%s -> %s\n", + strings.TrimSuffix(prompt, "\n"), + strings.TrimSpace(choices[selected].Text), + ) + + return choices[selected].Return, nil +} + +func removeDisplayLines(stdout io.Writer, n int) { + for idx := 0; idx < n; idx++ { + 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 { + fmt.Fprintf(out, "\x1B[0;7m%s\x1B[0m", text) + } 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 := b.Input.Read(b.buf[b.fill:]) + if err != nil { + panic(err) + } + b.fill += n + + return b.getNextInput() //restart +} + +//////////////////////////////////////////////////////////////////////////////// +// TUI implementation for when stdin is a pipe (also used in unit tests) + +type pipeTUI struct { + i *Implementation +} + +//AnsiColorCodeRx is a regexp that matches ANSI escape sequences of the type SGR. +var AnsiColorCodeRx = regexp.MustCompile("\x1B" + `\[[0-9;]*m`) + +func (t *pipeTUI) Print(w io.Writer, msg string) { + w.Write([]byte(AnsiColorCodeRx.ReplaceAllString(msg, ""))) +} + +func (t *pipeTUI) ReadLine(prompt string) (string, error) { + str, err := t.i.stdinBuf.ReadString('\n') + str = strings.TrimSpace(str) + if err == nil { + fmt.Fprintf(t.i.stderr, "%s %s\n", strings.TrimSpace(prompt), str) + } + return str, err +} + +func (t *pipeTUI) Confirm(question string) (bool, error) { + question = strings.TrimSpace(question) + str, err := t.ReadLine(question) + if err != nil { + return false, err + } + //recognize /[01tTfF]|true|false/ + ok, err := strconv.ParseBool(str) + if err != nil { + //recognize /[yY]|yes|no/ + ok = strings.HasPrefix(question, "y") || strings.HasPrefix(question, "Y") + } + fmt.Fprintf(t.i.stderr, "%s -> %v (%s)\n", question, ok, str) + return ok, nil +} + +//Query for the pipe TUI is limited to exact matches of the choice text, or +//matching by choice shortcut. +func (t *pipeTUI) Query(prompt string, choices ...Choice) (string, error) { + str, err := t.i.stdinBuf.ReadString('\n') + if err != nil { + return str, err + } + str = strings.TrimSpace(str) + + //prefer exact match on choice.Text + for _, choice := range choices { + if choice.Text == str { + fmt.Fprintf(t.i.stderr, "%s -> %s\n", prompt, choice.Text) + return choice.Return, nil + } + } + //allow match on choice.Shortcut + for _, choice := range choices { + if string(choice.Shortcut) == str { + fmt.Fprintf(t.i.stderr, "%s -> %s\n", prompt, choice.Text) + return choice.Return, nil + } + } + fmt.Fprintf(t.i.stderr, "%s -> [%s]\n", prompt, str) + return "", errors.New("cannot match input with available choices") +} |
