diff options
Diffstat (limited to 'pkg')
| -rw-r--r-- | pkg/cli/query.go | 186 | ||||
| -rw-r--r-- | pkg/cli/ui.go | 105 |
2 files changed, 291 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 +} diff --git a/pkg/cli/ui.go b/pkg/cli/ui.go new file mode 100644 index 0000000..c4eff69 --- /dev/null +++ b/pkg/cli/ui.go @@ -0,0 +1,105 @@ +/******************************************************************************* +* +* 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 ( + "bytes" + "strings" +) + +//AnsiStyle offers a set of styles for the VT100 SGR (Select Graphic Recognition) command. +type AnsiStyle string + +const ( + //AnsiNormal reverts all styles to default. + AnsiNormal AnsiStyle = "0" + //AnsiInverse swaps foreground and background color. + AnsiInverse AnsiStyle = "7" + //AnsiBlack is a foreground color. + AnsiBlack AnsiStyle = "30" + //AnsiRed is a foreground color. + AnsiRed AnsiStyle = "31" + //AnsiGreen is a foreground color. + AnsiGreen AnsiStyle = "32" + //AnsiYellow is a foreground color. + AnsiYellow AnsiStyle = "33" + //AnsiBlue is a foreground color. + AnsiBlue AnsiStyle = "34" + //AnsiMagenta is a foreground color. + AnsiMagenta AnsiStyle = "35" + //AnsiCyan is a foreground color. + AnsiCyan AnsiStyle = "36" + //AnsiWhite is a foreground color. + AnsiWhite AnsiStyle = "37" + //AnsiOnBlack is a background color. + AnsiOnBlack AnsiStyle = "40" + //AnsiOnRed is a background color. + AnsiOnRed AnsiStyle = "41" + //AnsiOnGreen is a background color. + AnsiOnGreen AnsiStyle = "42" + //AnsiOnYellow is a background color. + AnsiOnYellow AnsiStyle = "43" + //AnsiOnBlue is a background color. + AnsiOnBlue AnsiStyle = "44" + //AnsiOnMagenta is a background color. + AnsiOnMagenta AnsiStyle = "45" + //AnsiOnCyan is a background color. + AnsiOnCyan AnsiStyle = "46" + //AnsiOnWhite is a background color. + AnsiOnWhite AnsiStyle = "47" +) + +//StyledString is a string with attached style information for displaying on a terminal. +type StyledString struct { + Text string + Styles []AnsiStyle +} + +//Styled is syntactic sugar for a StyledString instance. +func Styled(text string, styles ...AnsiStyle) StyledString { + return StyledString{text, styles} +} + +//DisplayString returns the string for writing on a terminal emulator. +func (s StyledString) DisplayString(withReset bool) []byte { + strs := make([]string, len(s.Styles)) + for idx, s := range s.Styles { + strs[idx] = string(s) + } + str := "\x1B[" + strings.Join(strs, ";") + "m" + s.Text + if withReset { + str += "\x1B[0m" + } + return []byte(str) +} + +//StyledText is a sequence of styled strings. +type StyledText []StyledString + +//DisplayString returns the string for writing on a terminal emulator. +func (t StyledText) DisplayString(withReset bool) []byte { + var buf bytes.Buffer + for _, s := range t { + buf.Write(s.DisplayString(false)) + } + if withReset { + buf.Write([]byte("\x1B[0m")) + } + return buf.Bytes() +} |
